From 8b0e8c656d1c4bef31cd07d59501a4f0ae326efd Mon Sep 17 00:00:00 2001 From: Duy Nguyen <7498938+nthduy@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:56:52 +0100 Subject: [PATCH 1/3] feat(mangafreak): add support for MangaFreak Add chapter and manga extractors for ww2.mangafreak.me with support for bonus chapters (e.g., 167e suffix). --- docs/supportedsites.md | 6 ++ gallery_dl/extractor/__init__.py | 1 + gallery_dl/extractor/mangafreak.py | 116 +++++++++++++++++++++++++++++ scripts/supportedsites.py | 1 + test/results/mangafreak.py | 48 ++++++++++++ 5 files changed, 172 insertions(+) create mode 100644 gallery_dl/extractor/mangafreak.py create mode 100644 test/results/mangafreak.py diff --git a/docs/supportedsites.md b/docs/supportedsites.md index cebbc033..958f14db 100644 --- a/docs/supportedsites.md +++ b/docs/supportedsites.md @@ -667,6 +667,12 @@ Consider all listed sites to potentially be NSFW. Chapters, Manga + + MangaFreak + https://ww2.mangafreak.me/ + Chapters, Manga + + MangaPark https://mangapark.net/ diff --git a/gallery_dl/extractor/__init__.py b/gallery_dl/extractor/__init__.py index b57d7517..465f0e31 100644 --- a/gallery_dl/extractor/__init__.py +++ b/gallery_dl/extractor/__init__.py @@ -127,6 +127,7 @@ modules = [ "mangadex", "mangafire", "mangafox", + "mangafreak", "mangahere", "manganelo", "mangapark", diff --git a/gallery_dl/extractor/mangafreak.py b/gallery_dl/extractor/mangafreak.py new file mode 100644 index 00000000..ac6f0e5c --- /dev/null +++ b/gallery_dl/extractor/mangafreak.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +# Copyright 2025 Mike Fährmann +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. + +"""Extractors for https://ww2.mangafreak.me/""" + +from .common import ChapterExtractor, MangaExtractor +from .. import text + +BASE_PATTERN = r"(?:https?://)?(?:ww\d\.)?mangafreak\.me" + + +class MangafreakBase(): + """Base class for mangafreak extractors""" + category = "mangafreak" + root = "https://ww2.mangafreak.me" + + +class MangafreakChapterExtractor(MangafreakBase, ChapterExtractor): + """Extractor for mangafreak manga chapters""" + pattern = BASE_PATTERN + r"(/Read1_(.+)_(\d+[a-z]?))" + example = "https://ww2.mangafreak.me/Read1_Onepunch_Man_1" + + def __init__(self, match): + ChapterExtractor.__init__(self, match, self.root + match.group(1)) + self.manga_slug, self.chapter = match.groups()[1:] + + def metadata(self, page): + extr = text.extract_from(page) + manga = text.unescape(extr("", " Chapter ")) + title = text.unescape(extr("", " - MangaFreak")) + chapter_str = extr("# ", " MANGA ONLINE") + + # Parse chapter number and minor suffix (e.g., "167e" -> chapter=167, minor="e") + chapter, sep, minor = self.chapter.partition("e") if "e" in self.chapter else (self.chapter, "", "") + + return { + "manga" : manga, + "title" : title, + "chapter" : text.parse_int(chapter), + "chapter_minor": sep + minor, + "chapter_string": self.chapter, + "manga_slug" : self.manga_slug, + "lang" : "en", + "language" : "English", + } + + def images(self, page): + # Extract all <img> tags pointing to manga images + return [ + (url, None) + for url in text.extract_iter(page, '<img src="https://images.mangafreak.me/mangas/', '"') + ] + + +class MangafreakMangaExtractor(MangafreakBase, MangaExtractor): + """Extractor for mangafreak manga series""" + chapterclass = MangafreakChapterExtractor + pattern = BASE_PATTERN + r"(/Manga/([^/?#]+))" + example = "https://ww2.mangafreak.me/Manga/Onepunch_Man" + + def __init__(self, match): + MangaExtractor.__init__(self, match, self.root + match.group(1)) + self.manga_slug = match.group(2) + + def chapters(self, page): + extr = text.extract_from(page) + manga = text.unescape(extr("<title>", " Manga")) + + # Extract chapter list from table + chapter_list = text.extr(page, "<tbody>", "</tbody>") + if not chapter_list: + return [] + + data = { + "manga" : manga, + "manga_slug" : self.manga_slug, + "lang" : "en", + "language" : "English", + } + + results = [] + for row in text.extract_iter(chapter_list, "<tr>", "</tr>"): + # Extract chapter link and date from each row + chapter_link = text.extr(row, '<a href="', '"') + if not chapter_link: + continue + + # Build full URL if relative + if chapter_link.startswith("/"): + url = self.root + chapter_link + else: + url = self.root + "/" + chapter_link + + # Parse chapter info from URL like /Read1_Onepunch_Man_167e + chapter_part = url.rsplit("/", 1)[-1] # Read1_Onepunch_Man_167e + if chapter_part.startswith("Read1_"): + parts = chapter_part.split("_") + if len(parts) >= 3: + chapter_str = parts[-1] + # Parse chapter number and minor suffix + chapter, sep, minor = chapter_str.partition("e") if "e" in chapter_str else (chapter_str, "", "") + + chapter_data = { + "chapter" : text.parse_int(chapter), + "chapter_minor": sep + minor, + "chapter_string": chapter_str, + **data, + } + results.append((url, chapter_data)) + + return results diff --git a/scripts/supportedsites.py b/scripts/supportedsites.py index 0f950c06..797540a8 100755 --- a/scripts/supportedsites.py +++ b/scripts/supportedsites.py @@ -115,6 +115,7 @@ CATEGORY_MAP = { "kabeuchi" : "かべうち", "kaliscan" : "KaliScan", "mangafire" : "MangaFire", + "mangafreak" : "MangaFreak", "mangareader" : "MangaReader", "mangataro" : "MangaTaro", "s3ndpics" : "S3ND", diff --git a/test/results/mangafreak.py b/test/results/mangafreak.py new file mode 100644 index 00000000..3466bb0f --- /dev/null +++ b/test/results/mangafreak.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. + +from gallery_dl.extractor import mangafreak + + +__tests__ = ( +{ + "#url" : "https://ww2.mangafreak.me/Read1_Onepunch_Man_1", + "#class" : mangafreak.MangafreakChapterExtractor, + "#pattern" : r"https://images\.mangafreak\.me/mangas/onepunch_man/onepunch_man_1/onepunch_man_1_\d+\.jpg", + "#count" : 24, + + "chapter" : 1, + "chapter_minor": "", + "chapter_string": "1", + "lang" : "en", + "language" : "English", + "manga" : "Onepunch Man", + "manga_slug" : "Onepunch_Man", +}, + +{ + "#url" : "https://ww2.mangafreak.me/Read1_Onepunch_Man_167e", + "#class" : mangafreak.MangafreakChapterExtractor, + + "chapter" : 167, + "chapter_minor": "e", + "chapter_string": "167e", +}, + +{ + "#url" : "https://ww2.mangafreak.me/Manga/Onepunch_Man", + "#class" : mangafreak.MangafreakMangaExtractor, + "#pattern" : mangafreak.MangafreakChapterExtractor.pattern, + "#count" : range(150, 250), + + "lang" : "en", + "language" : "English", + "manga" : "Onepunch-Man", + "manga_slug" : "Onepunch_Man", + "chapter" : int, +}, + +) From 58662f900a149b3acb2e17e2eff218b57242cba5 Mon Sep 17 00:00:00 2001 From: Duy Nguyen <7498938+nthduy@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:24:05 +0100 Subject: [PATCH 2/3] fix(mangafreak): fix image extraction and simplify code - Fix image URL extraction pattern to handle img tags with id attribute - Use self.groups pattern instead of custom __init__ methods - Fix chapter list extraction to use correct table structure --- gallery_dl/extractor/mangafreak.py | 74 ++++++++++-------------------- 1 file changed, 25 insertions(+), 49 deletions(-) diff --git a/gallery_dl/extractor/mangafreak.py b/gallery_dl/extractor/mangafreak.py index ac6f0e5c..1ea2cd50 100644 --- a/gallery_dl/extractor/mangafreak.py +++ b/gallery_dl/extractor/mangafreak.py @@ -25,35 +25,30 @@ class MangafreakChapterExtractor(MangafreakBase, ChapterExtractor): pattern = BASE_PATTERN + r"(/Read1_(.+)_(\d+[a-z]?))" example = "https://ww2.mangafreak.me/Read1_Onepunch_Man_1" - def __init__(self, match): - ChapterExtractor.__init__(self, match, self.root + match.group(1)) - self.manga_slug, self.chapter = match.groups()[1:] - def metadata(self, page): extr = text.extract_from(page) manga = text.unescape(extr("<title>", " Chapter ")) title = text.unescape(extr("", " - MangaFreak")) - chapter_str = extr("# ", " MANGA ONLINE") - # Parse chapter number and minor suffix (e.g., "167e" -> chapter=167, minor="e") - chapter, sep, minor = self.chapter.partition("e") if "e" in self.chapter else (self.chapter, "", "") + chapter_str = self.groups[2] + chapter, sep, minor = chapter_str.partition("e") return { "manga" : manga, "title" : title, "chapter" : text.parse_int(chapter), "chapter_minor": sep + minor, - "chapter_string": self.chapter, - "manga_slug" : self.manga_slug, + "chapter_string": chapter_str, + "manga_slug" : self.groups[1], "lang" : "en", "language" : "English", } def images(self, page): - # Extract all <img> tags pointing to manga images return [ - (url, None) - for url in text.extract_iter(page, '<img src="https://images.mangafreak.me/mangas/', '"') + ("https://images.mangafreak.me/mangas/" + path, None) + for path in text.extract_iter( + page, 'src="https://images.mangafreak.me/mangas/', '"') ] @@ -63,54 +58,35 @@ class MangafreakMangaExtractor(MangafreakBase, MangaExtractor): pattern = BASE_PATTERN + r"(/Manga/([^/?#]+))" example = "https://ww2.mangafreak.me/Manga/Onepunch_Man" - def __init__(self, match): - MangaExtractor.__init__(self, match, self.root + match.group(1)) - self.manga_slug = match.group(2) - def chapters(self, page): extr = text.extract_from(page) manga = text.unescape(extr("<title>", " Manga")) - # Extract chapter list from table - chapter_list = text.extr(page, "<tbody>", "</tbody>") - if not chapter_list: + chapter_table = text.extr(page, "<table>", "</table>") + if not chapter_table: return [] data = { - "manga" : manga, - "manga_slug" : self.manga_slug, - "lang" : "en", - "language" : "English", + "manga" : manga, + "manga_slug": self.groups[1], + "lang" : "en", + "language" : "English", } results = [] - for row in text.extract_iter(chapter_list, "<tr>", "</tr>"): - # Extract chapter link and date from each row - chapter_link = text.extr(row, '<a href="', '"') - if not chapter_link: + for row in text.extract_iter(chapter_table, "<tr>", "</tr>"): + href = text.extr(row, '<a href="', '"') + if not href: continue + url = self.root + href + chapter_str = url.rpartition("_")[2] + chapter, sep, minor = chapter_str.partition("e") - # Build full URL if relative - if chapter_link.startswith("/"): - url = self.root + chapter_link - else: - url = self.root + "/" + chapter_link - - # Parse chapter info from URL like /Read1_Onepunch_Man_167e - chapter_part = url.rsplit("/", 1)[-1] # Read1_Onepunch_Man_167e - if chapter_part.startswith("Read1_"): - parts = chapter_part.split("_") - if len(parts) >= 3: - chapter_str = parts[-1] - # Parse chapter number and minor suffix - chapter, sep, minor = chapter_str.partition("e") if "e" in chapter_str else (chapter_str, "", "") - - chapter_data = { - "chapter" : text.parse_int(chapter), - "chapter_minor": sep + minor, - "chapter_string": chapter_str, - **data, - } - results.append((url, chapter_data)) + results.append((url, { + "chapter" : text.parse_int(chapter), + "chapter_minor": sep + minor, + "chapter_string": chapter_str, + **data, + })) return results From 4e71e2f7e72626e76105798ffbeb5d879a24924c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20F=C3=A4hrmann?= <mike_faehrmann@web.de> Date: Sun, 25 Jan 2026 19:49:56 +0100 Subject: [PATCH 3/3] [mangafreak] update & fix - fix manga and title extraction - fix 'chapter_minor' - extend test results --- gallery_dl/extractor/mangafreak.py | 59 +++++++++++++----------------- test/results/mangafreak.py | 47 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/gallery_dl/extractor/mangafreak.py b/gallery_dl/extractor/mangafreak.py index 1ea2cd50..4e015563 100644 --- a/gallery_dl/extractor/mangafreak.py +++ b/gallery_dl/extractor/mangafreak.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2025 Mike Fährmann -# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. @@ -11,7 +9,7 @@ from .common import ChapterExtractor, MangaExtractor from .. import text -BASE_PATTERN = r"(?:https?://)?(?:ww\d\.)?mangafreak\.me" +BASE_PATTERN = r"(?:https?://)?(?:ww[\dw]\.)?mangafreak\.me" class MangafreakBase(): @@ -22,33 +20,30 @@ class MangafreakBase(): class MangafreakChapterExtractor(MangafreakBase, ChapterExtractor): """Extractor for mangafreak manga chapters""" - pattern = BASE_PATTERN + r"(/Read1_(.+)_(\d+[a-z]?))" + pattern = BASE_PATTERN + r"(/Read1_([^/?#]+)_((\d+)([a-z])?))" example = "https://ww2.mangafreak.me/Read1_Onepunch_Man_1" def metadata(self, page): - extr = text.extract_from(page) - manga = text.unescape(extr("<title>", " Chapter ")) - title = text.unescape(extr("", " - MangaFreak")) - - chapter_str = self.groups[2] - chapter, sep, minor = chapter_str.partition("e") + manga = text.extr(page, "<title>Read ", " Chapter ") + title = text.extr(page, 'selected="selected">', "<").partition(": ")[2] + _, manga_slug, chapter_string, chapter, minor = self.groups return { - "manga" : manga, - "title" : title, + "manga" : text.unescape(manga), + "manga_slug" : manga_slug, + "title" : text.unescape(title) if title else "", "chapter" : text.parse_int(chapter), - "chapter_minor": sep + minor, - "chapter_string": chapter_str, - "manga_slug" : self.groups[1], + "chapter_minor": "" if minor is None else minor, + "chapter_string": chapter_string, "lang" : "en", "language" : "English", } def images(self, page): + base = "https://images.mangafreak.me/mangas/" return [ - ("https://images.mangafreak.me/mangas/" + path, None) - for path in text.extract_iter( - page, 'src="https://images.mangafreak.me/mangas/', '"') + (base + path, None) + for path in text.extract_iter(page, 'src="' + base, '"') ] @@ -59,34 +54,32 @@ class MangafreakMangaExtractor(MangafreakBase, MangaExtractor): example = "https://ww2.mangafreak.me/Manga/Onepunch_Man" def chapters(self, page): - extr = text.extract_from(page) - manga = text.unescape(extr("<title>", " Manga")) - - chapter_table = text.extr(page, "<table>", "</table>") - if not chapter_table: - return [] + table = text.extr(page, "<table>", "</table>") + if not table: + return () data = { - "manga" : manga, + "manga" : text.unescape(text.extr(page, "<title>", " Manga")), "manga_slug": self.groups[1], "lang" : "en", "language" : "English", } results = [] - for row in text.extract_iter(chapter_table, "<tr>", "</tr>"): + chapter_match = text.re(r"(\d+)(\w*)").match + for row in text.extract_iter(table, "<tr>", "</tr>"): href = text.extr(row, '<a href="', '"') if not href: continue url = self.root + href - chapter_str = url.rpartition("_")[2] - chapter, sep, minor = chapter_str.partition("e") - + chapter_string = href.rpartition("_")[2] + chapter, minor = chapter_match(chapter_string).groups() + title = text.extr(row, '">', '<').partition(" - ")[2] results.append((url, { - "chapter" : text.parse_int(chapter), - "chapter_minor": sep + minor, - "chapter_string": chapter_str, + "chapter" : text.parse_int(chapter), + "chapter_minor" : minor, + "chapter_string": chapter_string, + "title" : text.unescape(title) if title else "", **data, })) - return results diff --git a/test/results/mangafreak.py b/test/results/mangafreak.py index 3466bb0f..0478f8aa 100644 --- a/test/results/mangafreak.py +++ b/test/results/mangafreak.py @@ -32,6 +32,37 @@ __tests__ = ( "chapter_string": "167e", }, +{ + "#url" : "https://ww2.mangafreak.me/Read1_Sss_Rank_Dungeon_De_Knife_Ippon_Tewatasare_Tsuihou_Sareta_Shiro_Madoushi_Yggdrasil_No_Noroi_Ni_Yori_Jakuten_De_Aru_Maryoku_Fusoku_Wo_Kokufuku_Shi_Sekai_Saikyou_E_To_Itaru_23c", + "#class" : mangafreak.MangafreakChapterExtractor, + "#pattern" : r"https://images\.mangafreak\.me/mangas/sss_rank_dungeon_de_knife_ippon_tewatasare_tsuihou_sareta_shiro_madoushi_yggdrasil_no_noroi_ni_yori_jakuten_de_aru_maryoku_fusoku_wo_kokufuku_shi_sekai_saikyou_e_to_itaru/sss_rank_dungeon_de_knife_ippon_tewatasare_tsuihou_sareta_shiro_madoushi_yggdrasil_no_noroi_ni_yori_jakuten_de_aru_maryoku_fusoku_wo_kokufuku_shi_sekai_saikyou_e_to_itaru_23c/sss_rank_dungeon_de_knife_ippon_tewatasare_tsuihou_sareta_shiro_madoushi_yggdrasil_no_noroi_ni_yori_jakuten_de_aru_maryoku_fusoku_wo_kokufuku_shi_sekai_saikyou_e_to_itaru_23c_\d+\.jpg", + "#count" : 11, + + "chapter" : 23, + "chapter_minor" : "c", + "chapter_string": "23c", + "count" : 11, + "page" : range(1, 11), + "filename" : str, + "extension" : "jpg", + "lang" : "en", + "language" : "English", + "manga" : "Sss Rank Dungeon De Knife Ippon Tewatasare Tsuihou Sareta Shiro Madoushi Yggdrasil No Noroi Ni Yori Jakuten De Aru Maryoku Fusoku Wo Kokufuku Shi Sekai Saikyou E To Itaru", + "manga_slug" : "Sss_Rank_Dungeon_De_Knife_Ippon_Tewatasare_Tsuihou_Sareta_Shiro_Madoushi_Yggdrasil_No_Noroi_Ni_Yori_Jakuten_De_Aru_Maryoku_Fusoku_Wo_Kokufuku_Shi_Sekai_Saikyou_E_To_Itaru", + "title" : "", +}, + +{ + "#url" : "https://ww2.mangafreak.me/Read1_Tensei_Shitara_Slime_Datta_Ken_62", + "#class" : mangafreak.MangafreakChapterExtractor, + "#count" : 19, + + "chapter" : 62, + "count" : 19, + "manga" : "Tensei Shitara Slime Datta Ken", + "title" : "To be a Monster or Human", +}, + { "#url" : "https://ww2.mangafreak.me/Manga/Onepunch_Man", "#class" : mangafreak.MangafreakMangaExtractor, @@ -45,4 +76,20 @@ __tests__ = ( "chapter" : int, }, +{ + "#url" : "https://ww2.mangafreak.me/Manga/Sss_Rank_Dungeon_De_Knife_Ippon_Tewatasare_Tsuihou_Sareta_Shiro_Madoushi_Yggdrasil_No_Noroi_Ni_Yori_Jakuten_De_Aru_Maryoku_Fusoku_Wo_Kokufuku_Shi_Sekai_Saikyou_E_To_Itaru", + "#class" : mangafreak.MangafreakMangaExtractor, + "#pattern" : mangafreak.MangafreakChapterExtractor.pattern, + "#count" : range(40, 80), + + "chapter" : int, + "chapter_minor" : {"", "a", "b", "c"}, + "chapter_string": str, + "lang" : "en", + "language" : "English", + "manga" : "SSS Rank Dungeon de Knife Ippon Tewatasare Tsuihou Sareta Shiro Madoushi: Yggdrasil no Noroi ni yori Jakuten de aru Maryoku Fusoku wo Kokufuku-shi Sekai Saikyou e to Itaru", + "manga_slug" : "Sss_Rank_Dungeon_De_Knife_Ippon_Tewatasare_Tsuihou_Sareta_Shiro_Madoushi_Yggdrasil_No_Noroi_Ni_Yori_Jakuten_De_Aru_Maryoku_Fusoku_Wo_Kokufuku_Shi_Sekai_Saikyou_E_To_Itaru", + "title" : str, +}, + )