forked from MangaManagerORG/Manga-Manager
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cover downloader First implementation (MangaManagerORG#201)
* Fixed issue opening folder after other folder * spec improvements. * Versioning stuff * First implementation of CoverDownloader * Bump version hash * Modified build configurations * Bump version hash * Modified build configurations * Bump version hash * Bump version hash
- Loading branch information
1 parent
29248b0
commit dccbbdd
Showing
17 changed files
with
460 additions
and
79 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import dataclasses | ||
from pathlib import Path | ||
from os.path import splitext | ||
@dataclasses.dataclass | ||
class SeriesCoverData: | ||
normalized_name:str | ||
|
||
class CoverData: | ||
# filename:str | ||
# extension:str | ||
# | ||
# volume:str | ||
# locale:str | ||
@property | ||
def dest_filename(self): | ||
""" | ||
The final filename | ||
:return: | ||
""" | ||
return f"Cover{'_Vol.'+str(self.volume).zfill(2) if self.volume else ''}{'_' + self.locale if self.locale else ''}{self.extension}" | ||
|
||
def exists(self,dest_path): | ||
return Path(dest_path,self.dest_filename).exists() | ||
|
||
def __init__(self,source_filename=None,volume=None,locale=None): | ||
|
||
self.source_filename=source_filename | ||
if source_filename: | ||
self.filename, self.extension = splitext(self.source_filename) | ||
else: | ||
self.filename, self.extension = None, None | ||
self.volume=volume | ||
self.locale=locale | ||
|
||
|
||
def from_cover_data(self,cover_data): | ||
self.source_filename = cover_data.get("attributes").get("fileName") | ||
self.filename, self.extension = splitext(self.source_filename) | ||
self.volume = cover_data.get("attributes").get("volume") | ||
self.locale = cover_data.get("attributes").get("locale") | ||
return self |
122 changes: 111 additions & 11 deletions
122
MangaManager/Extensions/CoverDownloader/CoverDownloader.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,127 @@ | ||
from tkinter import Label, Frame, Entry | ||
|
||
# import tkinter | ||
# from idlelib.tooltip import Hovertip | ||
# from idlelib.tooltip import Hovertip | ||
import logging | ||
import urllib.request | ||
import os | ||
import platform | ||
import subprocess | ||
from enum import StrEnum | ||
from pathlib import Path | ||
from tkinter import Frame, Label, StringVar | ||
from tkinter.ttk import Entry, Button, Frame | ||
import slugify | ||
import requests | ||
|
||
from Extensions.CoverDownloader.CoverData import CoverData | ||
from Extensions.CoverDownloader.MangaDex import parse_mangadex_id | ||
from Extensions.CoverDownloader.exceptions import UrlNotFound | ||
from Extensions.IExtensionApp import IExtensionApp | ||
# from src.MetadataManager.GUI.tooltip import ToolTip | ||
from src.Settings import Settings, SettingHeading, SettingSection, SettingControl, SettingControlType | ||
|
||
logger = logging.getLogger() | ||
def get_cover_from_source_dummy() -> list: | ||
... | ||
|
||
|
||
class CoverDownloader():#IExtensionApp): | ||
class CoverDownloaderSetting(StrEnum): | ||
ForceOverwrite = "force_overwrite" # If existing covers should be re-downloaded and overwritten | ||
class CoverDownloader(IExtensionApp): | ||
name = "Cover Downloader" | ||
embedded_ui = True | ||
|
||
def init_settings(self): | ||
|
||
self.settings = [ | ||
SettingSection(self.name,self.name,[ | ||
SettingControl( | ||
key=CoverDownloaderSetting.ForceOverwrite, | ||
name="Force Overwrite", | ||
control_type=SettingControlType.Bool, | ||
value=False, | ||
tooltip="Enable if existing covers should be re-downloaded and overwritten" | ||
) | ||
]) | ||
] | ||
super().init_settings() | ||
|
||
@property | ||
def output_folder(self): | ||
return Settings().get(SettingHeading.Main, 'covers_folder_path') | ||
|
||
def serve_gui(self): | ||
if not self.master: | ||
return Exception("Tried to initialize ui with no master window") | ||
self.url_var = StringVar(name="mdex_cover_url") | ||
self.dest_path_var = StringVar(name="dest_path",value=str(Path((self.output_folder or Path(Path.home(),"Pictures","Manga Covers")), | ||
'<Manga Name Folder>'))) | ||
Label(self.master,text="MangaManager will download all availabe images from").pack() | ||
# self.frame_3 = Frame(frame1) | ||
Label(self.master, text='MANGADEX MANGA ID / URL').pack(side='top') | ||
Entry(master=self.master,textvariable=self.url_var).pack(side='top') | ||
|
||
Label(self.master, text="to the folder:").pack() | ||
label_path = Label(self.master,textvariable=self.dest_path_var) | ||
label_path.pack(pady="10 30", side='top') | ||
|
||
self.button_1 = Button(self.master, text='Correct! Start!', command=self.process_download) | ||
self.button_1.pack(side='top') | ||
self.button_explorer = Button(self.master, text='Open explorer', command=self.open_explorer) | ||
|
||
def process_download(self): | ||
try: | ||
self.download() | ||
except Exception as e: | ||
logger.exception("Error downloading") | ||
finally: | ||
self.show_open_explorer_btn() | ||
|
||
def show_open_explorer_btn(self): | ||
self.button_explorer.pack(side='top') | ||
|
||
def open_explorer(self): | ||
|
||
frame = Frame(self.master) | ||
frame.pack() | ||
path = self.dest_path_var.get() | ||
if platform.system() == "Windows": | ||
os.startfile(path) | ||
elif platform.system() == "Darwin": | ||
subprocess.Popen(["open", path]) | ||
else: | ||
subprocess.Popen(["xdg-open", path]) | ||
... | ||
|
||
Label(frame, text="Manga identifier").pack() | ||
Entry(frame).pack() | ||
# Combobox(frame, state="readonly",values=sources_factory["CoverSources"]).pack() | ||
def download(self): | ||
... | ||
mdex_id = parse_mangadex_id(self.url_var.get()) | ||
logger.debug(f"Parsed mdex id: '{mdex_id}'") | ||
data = {"manga[]": [mdex_id], "includes[]": ["manga"], "limit": 50} | ||
# Request the list of covers in the prrovided manga | ||
r = requests.get(f"https://api.mangadex.org/cover", params=data) | ||
|
||
covers = get_cover_from_source_dummy() | ||
if r.status_code == 400: | ||
|
||
raise UrlNotFound(r.url) | ||
data = r.json().get("data") | ||
cover_attributes = data[0].get("relationships")[0].get("attributes") | ||
|
||
ja_title = list(filter(lambda p: p.get("ja-ro"), | ||
cover_attributes.get("altTitles"))) | ||
if ja_title: | ||
ja_title = ja_title[0].get("ja-ro") | ||
full_name = (ja_title or cover_attributes.get("title").get("en")) | ||
normalized_manga_name = slugify.slugify(full_name[:50]) | ||
destination_dirpath = Path((self.output_folder or Path(Path.home(), "Pictures", "Manga Covers")),normalized_manga_name) | ||
self.dest_path_var.set(str(destination_dirpath)) | ||
self.update() | ||
destination_dirpath.mkdir(parents=True, exist_ok=True) | ||
_total = len(data) | ||
|
||
for i,cover_data in enumerate(data): | ||
cover = CoverData().from_cover_data(cover_data) | ||
image_path = Path(destination_dirpath, cover.dest_filename) | ||
if not cover.exists(image_path) or Settings().get(self.name,CoverDownloaderSetting.ForceOverwrite): | ||
image_url = f"https://mangadex.org/covers/{mdex_id}/{cover.source_filename}" | ||
|
||
urllib.request.urlretrieve(image_url, image_path) | ||
logger.debug(f"Downloaded {image_path}") | ||
else: | ||
logger.info(f"Skipped https://mangadex.org/covers/{mdex_id}/{cover.source_filename} -> Already exists") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import requests | ||
|
||
from Extensions.CoverDownloader.exceptions import UrlNotFound | ||
|
||
|
||
def parse_mangadex_id(value) -> str: | ||
""" | ||
Accepts a mangadex id or URL | ||
:param value: | ||
:return: mangadex managa id | ||
""" | ||
|
||
if "https://mangadex.org/title/" in value: | ||
value = value.replace("https://mangadex.org/title/", "").split("/")[0] | ||
return value | ||
|
||
|
||
def download_covers(self, manga_id, forceOverwrite=False, test_run=False): | ||
""" | ||
Downloads the covers from manga_id from mangadex. | ||
If the cover is already downloaded it won't re-download | ||
:param manga_id: The manga id only. | ||
:param cover_folder: The folder where the series folder will be created. | ||
:param forceOverwrite: If existing covers should be re-downloaded and overwritten | ||
:param test_run: Value to make a dry run where it asserts that each cover url is valid | ||
:return: | ||
""" | ||
|
||
data = {"manga[]": [manga_id], "includes[]": ["manga"], "limit": 50} | ||
# Request the list of covers in the prrovided manga | ||
r = requests.get(f"https://api.mangadex.org/cover", params=data) | ||
|
||
if r.status_code == 400: | ||
raise UrlNotFound(r.url) | ||
|
||
r_json = r.json() | ||
cover_attributes = r_json.get("data")[0].get("relationships")[0].get("attributes") | ||
|
||
ja_title = list(filter(lambda p: p.get("ja-ro"), | ||
cover_attributes.get("altTitles"))) | ||
if ja_title: | ||
ja_title = ja_title[0].get("ja-ro") | ||
|
||
normalized_manga_name = (ja_title or cover_attributes.get("title").get("en")) | ||
|
||
|
||
|
||
|
||
destination_dirpath = pathlib.Path(self.settings.get('cover_folder_path'), cleanFilename( | ||
normalized_manga_name)) # The covers get stored in their own series folder inside the covers directory | ||
if not test_run: | ||
destination_dirpath.mkdir(parents=True, exist_ok=True) | ||
total = len(r_json.get("data")) | ||
self.progressbar = ProgressBar(self._initialized_UI, self._progressbar_frame, total) | ||
|
||
for i, cover_data in enumerate(r_json.get("data")): | ||
try: | ||
|
||
cover_filename = cover_data.get("attributes").get("fileName") | ||
filename, file_extension = os.path.splitext(cover_filename) | ||
|
||
cover_volume = cover_data.get("attributes").get("volume") | ||
cover_loc = cover_data.get("attributes").get("locale") | ||
|
||
destination_filename = f"Cover_Vol.{str(cover_volume).zfill(2)}_{cover_loc}{file_extension}" | ||
destination_filepath = pathlib.Path(destination_dirpath, destination_filename) | ||
if (not destination_filepath.exists() or forceOverwrite) and not test_run: | ||
image_url = f"https://mangadex.org/covers/{manga_id}/{cover_filename}" | ||
urllib.request.urlretrieve(image_url, destination_filepath) | ||
logger.debug(f"Downloaded {destination_filename}") | ||
elif test_run: | ||
image_url = f"https://mangadex.org/covers/{manga_id}/{cover_filename}" | ||
print(f"Asserting if valid url: '{image_url}' ") | ||
return check_url_isImage(image_url) | ||
else: | ||
logger.info(f"Skipped 'https://mangadex.org/covers/{manga_id}/{cover_filename}' -> Already exists") | ||
self.progressbar.increaseCount() | ||
except Exception as e: | ||
logger.error(e) | ||
self.progressbar.increaseError() | ||
logger.info(f"Files saved to: '{destination_dirpath}'") | ||
print(f"Files saved to: '{destination_dirpath}'") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
class CoverDoesNotExist(Exception): | ||
pass | ||
|
||
|
||
class NoOverwriteSelected(Exception): | ||
pass | ||
|
||
|
||
class NoCoverFile(FileNotFoundError): | ||
""" | ||
Exception raised when cover path is not specified or not found. | ||
""" | ||
|
||
def __init__(self, coverFilePath, parent_window=None): | ||
super().__init__(f'Cover image file path not provided or image not found: {coverFilePath}') | ||
|
||
class UrlNotFound(Exception): | ||
""" | ||
Exception raised when api returns 400 status code | ||
""" | ||
|
||
def __init__(self, requestUrl): | ||
super().__init__(f'Url {requestUrl} not found') |
Oops, something went wrong.