diff --git a/docs/supportedsites.md b/docs/supportedsites.md index 789495e6..71ec3701 100644 --- a/docs/supportedsites.md +++ b/docs/supportedsites.md @@ -592,7 +592,7 @@ Consider all listed sites to potentially be NSFW. MangaDex https://mangadex.org/ - Authors, Chapters, Updates Feed, Library, MDLists, Manga + Authors, Chapters, Covers, Updates Feed, Library, MDLists, Manga Supported diff --git a/gallery_dl/extractor/mangadex.py b/gallery_dl/extractor/mangadex.py index 225560dc..fbed328a 100644 --- a/gallery_dl/extractor/mangadex.py +++ b/gallery_dl/extractor/mangadex.py @@ -96,6 +96,57 @@ class MangadexExtractor(Extractor): return data +class MangadexCoversExtractor(MangadexExtractor): + """Extractor for mangadex manga covers""" + subcategory = "covers" + directory_fmt = ("{category}", "{manga}", "Covers") + filename_fmt = "{volume:>02}_{lang}.{extension}" + archive_fmt = "c_{cover_id}" + pattern = (rf"{BASE_PATTERN}/(?:title|manga)/(?!follows|feed$)([0-9a-f-]+)" + r"(?:/[^/?#]+)?\?tab=art") + example = ("https://mangadex.org/title" + "/01234567-89ab-cdef-0123-456789abcdef?tab=art") + + def items(self): + base = f"{self.root}/covers/{self.uuid}/" + for cover in self.api.covers_manga(self.uuid): + data = self._transform_cover(cover) + name = data["cover"] + text.nameext_from_url(name, data) + data["cover_id"] = data["filename"] + yield Message.Directory, data + yield Message.Url, f"{base}{name}", data + + def _transform_cover(self, cover): + relationships = defaultdict(list) + for item in cover["relationships"]: + relationships[item["type"]].append(item) + manga = self.api.manga(relationships["manga"][0]["id"]) + for item in manga["relationships"]: + relationships[item["type"]].append(item) + + cattributes = cover["attributes"] + mattributes = manga["attributes"] + + return { + "manga" : (mattributes["title"].get("en") or + next(iter(mattributes["title"].values()))), + "manga_id": manga["id"], + "status" : mattributes["status"], + "author" : [author["attributes"]["name"] + for author in relationships["author"]], + "artist" : [artist["attributes"]["name"] + for artist in relationships["artist"]], + "tags" : [tag["attributes"]["name"]["en"] + for tag in mattributes["tags"]], + "cover" : cattributes["fileName"], + "lang" : cattributes.get("locale"), + "volume" : text.parse_int(cattributes["volume"]), + "date" : text.parse_datetime(cattributes["createdAt"]), + "date_updated": text.parse_datetime(cattributes["updatedAt"]), + } + + class MangadexChapterExtractor(MangadexExtractor): """Extractor for manga-chapters from mangadex.org""" subcategory = "chapter" @@ -239,6 +290,10 @@ class MangadexAPI(): params = {"includes[]": ("scanlation_group",)} return self._call("/chapter/" + uuid, params)["data"] + def covers_manga(self, uuid): + params = {"manga[]": uuid} + return self._pagination_covers("/cover", params) + def list(self, uuid): return self._call("/list/" + uuid, None, True)["data"] @@ -374,6 +429,20 @@ class MangadexAPI(): return self._pagination(endpoint, params, auth) + def _pagination_covers(self, endpoint, params=None, auth=False): + if params is None: + params = {} + + lang = self.extractor.config("lang") + if isinstance(lang, str) and "," in lang: + lang = lang.split(",") + params["locales"] = lang + params["contentRating"] = None + params["order[volume]"] = \ + "desc" if self.extractor.config("chapter-reverse") else "asc" + + return self._pagination(endpoint, params, auth) + def _pagination(self, endpoint, params, auth=False): config = self.extractor.config diff --git a/test/results/mangadex.py b/test/results/mangadex.py index 62fccce8..b61360f4 100644 --- a/test/results/mangadex.py +++ b/test/results/mangadex.py @@ -167,4 +167,63 @@ __tests__ = ( "#count" : ">= 15", }, +{ + "#url" : "https://mangadex.org/title/f90c4398-8aad-4f51-8a1f-024ca09fdcbc?tab=art", + "#class" : mangadex.MangadexCoversExtractor, + "#results" : "https://mangadex.org/covers/f90c4398-8aad-4f51-8a1f-024ca09fdcbc/af3c1690-1e06-4432-909e-3e0f9ee01f68.jpg", + + "artist" : ["Arakawa Hiromu"], + "author" : ["Arakawa Hiromu"], + "cover" : "af3c1690-1e06-4432-909e-3e0f9ee01f68.jpg", + "cover_id" : "af3c1690-1e06-4432-909e-3e0f9ee01f68", + "date" : "dt:2021-05-24 17:19:13", + "date_updated": "dt:2021-05-24 17:19:13", + "extension" : "jpg", + "filename" : "af3c1690-1e06-4432-909e-3e0f9ee01f68", + "lang" : "ja", + "manga" : "Souten no Koumori", + "manga_id" : "f90c4398-8aad-4f51-8a1f-024ca09fdcbc", + "status" : "completed", + "volume" : 0, + "tags" : [ + "Oneshot", + "Historical", + "Action", + "Martial Arts", + "Drama", + "Tragedy", + ], +}, + +{ + "#url" : "https://mangadex.org/title/192aa767-2479-42c1-9780-8d65a2efd36a/gachiakuta?tab=art", + "#class" : mangadex.MangadexCoversExtractor, + "#pattern" : r"https://mangadex\.org/covers/192aa767-2479-42c1-9780-8d65a2efd36a/[\w-]+\.jpg", + "#count" : 19, + + "artist" : ["Urana Kei"], + "author" : ["Urana Kei"], + "cover_id" : "iso:uuid", + "date" : "type:datetime", + "date_updated": "type:datetime", + "extension" : "jpg", + "filename" : str, + "lang" : {"ja", "fa"}, + "manga" : "Gachiakuta", + "manga_id" : "192aa767-2479-42c1-9780-8d65a2efd36a", + "status" : "ongoing", + "volume" : range(1, 16), + "tags" : [ + "Monsters", + "Action", + "Comedy", + "Survival", + "Drama", + "Fantasy", + "Delinquents", + "Supernatural", + "Tragedy", + ], +}, + )