...
 
Commits (2)
......@@ -3,6 +3,8 @@ V 0.19.0:
- Adjusted to new xdcc-dl paradigms
- Major refactor of metadata structure
- Uses json schema to validate metadata
- Added music support
- Create unified Updater functionality
V 0.18.0:
- Redid metadata to be more minimalistic and easier to maintain
- Substituted verification for Checkers
......
......@@ -25,9 +25,9 @@ from puffotter.prompt import yn_prompt
from toktokkie.renaming.Renamer import Renamer
from toktokkie.iconizing.Iconizer import Iconizer, Procedure
from toktokkie.metadata.functions import get_metadata, create_metadata
from toktokkie.metadata.MediaType import MediaType
from toktokkie.exceptions import MissingMetadata
from toktokkie.xdcc_update.XDCCUpdater import XDCCUpdater
from toktokkie.exceptions import MissingMetadata, InvalidUpdateInstructions, \
MissingUpdateInstructions
from toktokkie.update import updaters
from toktokkie.check.map import checker_map
......@@ -138,18 +138,32 @@ class Directory:
)
return checker.check()
def xdcc_update(self, throttle: int, timeout: int):
def update(self, args: Dict[str, Any]):
"""
Performs an XDCC Update Action
:param throttle: The throttle value
:param timeout: The timeout value
Performs an Update Action
:param args: Command line arguments used to configure the update
:return: None
"""
if self.metadata.media_type() == MediaType.TV_SERIES:
# noinspection PyTypeChecker
xdcc = XDCCUpdater(
self.metadata, throttle, timeout # type: ignore
)
xdcc.update()
else:
self.logger.warning("xdcc-update is only supported for TV series")
applicable_updaters = [
x for x in updaters
if self.metadata.media_type() in x.applicable_media_types()
]
for updater_cls in applicable_updaters:
if args["create"]:
updater_cls.prompt(self.metadata)
else:
try:
updater = updater_cls(self.metadata, args)
print("Updating {} using {} updater:".format(
self.metadata.name, updater.name()
))
updater.update()
except MissingUpdateInstructions:
self.logger.warning("No update instructions for {}"
.format(self.path))
except InvalidUpdateInstructions as e:
self.logger.warning(
"Update instructions for {} are invalid: {}"
.format(self.path, e)
)
......@@ -40,14 +40,14 @@ class MissingMetadata(Exception):
pass
class MissingXDCCInstructions(Exception):
class MissingUpdateInstructions(Exception):
"""
Exception that is raised whenever xdcc update instructions don't exist
"""
pass
class InvalidXDCCInstructions(Exception):
class InvalidUpdateInstructions(Exception):
"""
Exception that is raised whenever xdcc update instructions are invalid
"""
......
......@@ -19,14 +19,11 @@ LICENSE"""
import argparse
from toktokkie.scripts.Command import Command
from toktokkie.xdcc_update.XDCCUpdater import XDCCUpdater
from toktokkie.exceptions import MissingXDCCInstructions, \
InvalidXDCCInstructions
class XdccUpdateCommand(Command):
class UpdateCommand(Command):
"""
Class that encapsulates behaviour of the xdcc-update command
Class that encapsulates behaviour of the update command
"""
@classmethod
......@@ -34,7 +31,7 @@ class XdccUpdateCommand(Command):
"""
:return: The command name
"""
return "xdcc-update"
return "update"
@classmethod
def prepare_parser(cls, parser: argparse.ArgumentParser):
......@@ -44,14 +41,28 @@ class XdccUpdateCommand(Command):
:return: None
"""
cls.add_directories_arg(parser)
parser.add_argument("--dry-run", action="store_true",
help="Does not download or rename anything")
# xdcc
parser.add_argument("--create", action="store_true",
help="If this flag is set, "
"will generate new xdcc update instructions")
"will generate new update instructions")
parser.add_argument("-t", "--throttle", default=-1,
help="Limits the download speed of xdcc-dl. "
"Append K,M or G for more convenient units")
parser.add_argument("--timeout", default=120, type=int,
help="Sets a timeout for starting the download")
help="Sets a timeout for starting "
"the xdcc-dl download")
# manga
parser.add_argument("--no-check-newest-chapter-length",
action="store_true",
help="Deactivates checking the latest manga "
"chapter for completeness")
parser.add_argument("--skip-special", action="store_true",
help="Skips updating special manga chapters")
def execute(self):
"""
......@@ -59,17 +70,4 @@ class XdccUpdateCommand(Command):
:return: None
"""
for directory in self.load_directories(self.args.directories):
try:
if self.args.create:
XDCCUpdater.prompt(directory.metadata)
else:
directory.xdcc_update(
self.args.throttle, self.args.timeout
)
except MissingXDCCInstructions:
self.logger.warning("No XDCC update instructions for {}"
.format(directory.path))
except InvalidXDCCInstructions:
self.logger.warning("Invalid XDCC update instructions for {}"
.format(directory.path))
directory.update(dict(self.args.__dict__))
......@@ -24,9 +24,8 @@ from toktokkie.scripts.RenameCommand import RenameCommand
from toktokkie.scripts.ArchiveCommand import ArchiveCommand
from toktokkie.scripts.CheckCommand import CheckCommand
from toktokkie.scripts.MangaCreateCommand import MangaCreateCommand
from toktokkie.scripts.MangaUpdateCommand import MangaUpdateCommand
from toktokkie.scripts.MetadataGenCommand import MetadataGenCommand
from toktokkie.scripts.XdccUpdateCommand import XdccUpdateCommand
from toktokkie.scripts.UpdateCommand import UpdateCommand
from toktokkie.scripts.MetadataAddCommand import MetadataAddCommand
from toktokkie.scripts.SetMangaCoverCommand import SetMangaCoverCommand
from toktokkie.scripts.SuperCutCommand import SuperCutCommand
......@@ -43,10 +42,9 @@ toktokkie_commands = [
RenameCommand,
ArchiveCommand,
CheckCommand,
MangaUpdateCommand,
MangaCreateCommand,
MetadataGenCommand,
XdccUpdateCommand,
UpdateCommand,
MetadataAddCommand,
SetMangaCoverCommand,
SuperCutCommand,
......
......@@ -18,104 +18,85 @@ along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import os
import argparse
from typing import List, Optional
from toktokkie.metadata.types.Manga import Manga
from toktokkie.metadata.ids.IdType import IdType
from toktokkie.metadata.MediaType import MediaType
from zipfile import ZipFile
from typing import List, Optional, cast
from manga_dl.scrapers.mangadex import MangaDexScraper
from manga_dl.entities.Chapter import Chapter
from puffotter.os import makedirs, listdir, replace_illegal_ntfs_chars
from puffotter.print import pprint
from toktokkie.Directory import Directory
from toktokkie.scripts.Command import Command
from toktokkie.metadata.ids.IdType import IdType
from toktokkie.metadata.MediaType import MediaType
from toktokkie.metadata.types.Manga import Manga
from toktokkie.renaming.Renamer import Renamer
from zipfile import ZipFile
from toktokkie.update.Updater import Updater
class MangaUpdateCommand(Command):
class MangadexUpdater(Updater):
"""
Class that encapsulates behaviour of the manga-update command
Class that implements a mangadex updater
"""
@classmethod
def name(cls) -> str:
"""
:return: The command name
:return: The name of the Updater
"""
return "manga-update"
return "mangadex"
@classmethod
def prepare_parser(cls, parser: argparse.ArgumentParser):
def applicable_media_types(cls) -> List[MediaType]:
"""
Prepares an argumentparser for this command
:param parser: The parser to prepare
:return: None
:return: A list of media type with which the updater can be used with
"""
cls.add_directories_arg(parser)
parser.add_argument("--dry-run", action="store_true",
help="Does not download or rename anything")
parser.add_argument("--no-check-newest-chapter-length",
action="store_true",
help="Deactivates checking the latest chapter "
"for completeness")
parser.add_argument("--skip-special", action="store_true",
help="Skips updating special chapters")
def execute(self):
return [MediaType.MANGA]
def update(self):
"""
Executes the commands
Executes the update
:return: None
"""
for directory in self.load_directories(
self.args.directories, [MediaType.MANGA]
):
metadata = directory.metadata # type: Manga
print(metadata.name)
self.execute_rename()
self.execute_rename(directory)
chapters = self.load_chapters(metadata)
if chapters is None:
continue
chapters = self.load_chapters()
if chapters is None:
return
if not self.args.no_check_newest_chapter_length:
self.check_latest_chapter_completeness(metadata, chapters)
self.update_main_chapters(metadata, chapters)
if not self.args["no_check_newest_chapter_length"]:
self.check_latest_chapter_completeness(chapters)
self.update_main_chapters(chapters)
if not self.args.skip_special:
self.update_special_chapters(metadata, chapters)
if not self.args["skip_special"]:
self.update_special_chapters(chapters)
if not self.args.dry_run:
self.execute_rename(directory)
if not self.args["dry_run"]:
self.execute_rename()
def execute_rename(self, directory: Directory):
def execute_rename(self):
"""
Renames the current directory content. Automatically checks if the
dry-run flag is set. If it is set, prints out any chapters that
would have been renamed
:param directory: The directory whose conet should be renamed
:return: None
"""
self.logger.info("Running rename")
if not self.args.dry_run:
directory.rename(noconfirm=True)
if not self.args["dry_run"]:
Renamer(self.metadata).rename(noconfirm=True)
else:
ops = Renamer(directory.metadata).get_active_operations()
ops = Renamer(self.metadata).get_active_operations()
for op in ops:
print(op)
@staticmethod
def load_chapters(metadata: Manga) -> Optional[List[Chapter]]:
def load_chapters(self) -> Optional[List[Chapter]]:
"""
Loads chapter information from mangadex.org
:param metadata: The metadata for which to load the chapters
:return: Either a list of chapters or None if no mangadex ID was found
"""
mangadex_ids = metadata.ids.get(IdType.MANGADEX)
mangadex_ids = self.metadata.ids.get(IdType.MANGADEX)
if mangadex_ids is None or len(mangadex_ids) == 0:
pprint("No mangadex ID for {}".format(metadata.name), fg="lred")
pprint(
"No mangadex ID for {}".format(self.metadata.name), fg="lred"
)
return None
mangadex_id = mangadex_ids[0]
......@@ -125,7 +106,6 @@ class MangaUpdateCommand(Command):
def check_latest_chapter_completeness(
self,
metadata: Manga,
chapters: List[Chapter]
):
"""
......@@ -133,10 +113,10 @@ class MangaUpdateCommand(Command):
This is necessary since some groups release their chapters in parts
(i.e. 10.1 and then 10.2). manga-dl merges these chapter parts, so
we only need to check if the local files has less pages.
:param metadata: The metadata of the series to check
:param chapters: The chapters for the series
:return: None
"""
metadata = cast(Manga, self.metadata)
main_chapters = list(filter(lambda x: not x.is_special, chapters))
current_files = listdir(metadata.main_path)
......@@ -159,7 +139,7 @@ class MangaUpdateCommand(Command):
pagecount = len(current_chapter.pages)
if filecount < pagecount:
if not self.args.dry_run:
if not self.args["dry_run"]:
pprint("Updating chapter {}".format(current_chapter),
fg="lgreen")
os.remove(past_file)
......@@ -175,13 +155,14 @@ class MangaUpdateCommand(Command):
)
)
def update_main_chapters(self, metadata: Manga, chapters: List[Chapter]):
def update_main_chapters(self, chapters: List[Chapter]):
"""
Updates the regular chapters of the series
:param metadata: The metadata of the series
:param chapters: The chapters of the series
:return: None
"""
metadata = cast(Manga, self.metadata)
current_latest = len(listdir(metadata.main_path))
main_chapters = list(filter(
......@@ -214,7 +195,7 @@ class MangaUpdateCommand(Command):
c.chapter_number.zfill(len(str(total_chapters)))
)
dest = os.path.join(metadata.main_path, name)
if not self.args.dry_run:
if not self.args["dry_run"]:
print("Downloading Chapter {}".format(c))
c.download(dest)
else:
......@@ -222,15 +203,14 @@ class MangaUpdateCommand(Command):
def update_special_chapters(
self,
metadata: Manga,
chapters: List[Chapter]
):
"""
Updates the special chapters of the series
:param metadata: The metadata of the series
:param chapters: The chapters of the series
:return: None
"""
metadata = cast(Manga, self.metadata)
special_chapters = list(filter(lambda x: x.is_special, chapters))
try:
......@@ -256,7 +236,7 @@ class MangaUpdateCommand(Command):
continue
elif c.chapter_number not in metadata.special_chapters:
if self.args.dry_run:
if self.args["dry_run"]:
pprint("Found unknown chapter {}".format(c.chapter_number),
fg="lyellow")
else:
......@@ -267,10 +247,12 @@ class MangaUpdateCommand(Command):
metadata.special_chapters = chapter_entries
metadata.write()
makedirs(metadata.special_path)
print("Downloading special chapter {}".format(c))
c.download(path)
else:
if not self.args.dry_run:
if not self.args["dry_run"]:
makedirs(metadata.special_path)
print("Downloading special chapter {}".format(c))
c.download(path)
else:
pprint("Found chapter: {}".format(c), fg="lyellow")
"""LICENSE
Copyright 2015 Hermann Krumrey <hermann@krumreyh.com>
This file is part of toktokkie.
toktokkie is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
toktokkie is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import os
import json
import logging
from typing import Dict, Any, List, Optional
from jsonschema import validate, ValidationError
from toktokkie.metadata.Metadata import Metadata
from toktokkie.metadata.MediaType import MediaType
from toktokkie.exceptions import InvalidUpdateInstructions, \
MissingUpdateInstructions
class Updater:
"""
Class that defines the common behaviour of Updaters
"""
def __init__(self, metadata: Metadata, args: Dict[str, Any]):
"""
Initializes the Updater object
:param metadata: The metadata belonging to the media to update
:param args: The command line arguments used to configure the Updater
"""
self.logger = logging.getLogger(self.__class__.__name__)
self.metadata = metadata
self.args = args
self.update_file = os.path.join(
metadata.directory_path,
".meta/{}_update.json".format(self.name())
)
self.config = {} # type: Dict[str, Any]
if self.json_schema() is not None:
if os.path.isfile(self.update_file):
with open(self.update_file, "r") as f:
self.config = json.load(f)
else:
raise MissingUpdateInstructions(self.update_file)
self.validate()
@classmethod
def name(cls) -> str:
"""
:return: The name of the Updater
"""
raise NotImplementedError()
@classmethod
def applicable_media_types(cls) -> List[MediaType]:
"""
:return: A list of media type with which the updater can be used with
"""
raise NotImplementedError()
@classmethod
def json_schema(cls) -> Optional[Dict[str, Any]]:
"""
:return: Optional JSON schema for a configuration file
"""
return None
@classmethod
def prompt(cls, metadata: Metadata):
"""
Prompts the user for information to create a config file
:param metadata: The metadata of the media for which to create an
updater config file
:return: None
"""
config = cls._prompt(metadata)
if config is not None:
update_file = os.path.join(
metadata.directory_path,
".meta/{}_update.json".format(cls.name())
)
with open(update_file, "w") as f:
f.write(json.dumps(
config,
sort_keys=True,
indent=4,
separators=(",", ": ")
))
# noinspection PyUnusedLocal
@classmethod
def _prompt(cls, metadata: Metadata) -> Optional[Dict[str, Any]]:
"""
Prompts the user for information to create a config file
This method is meant to be overridden by subclasses
:param metadata: The metadata of the media for which to create an
updater config file
:return: The configuration JSON data
"""
logging.getLogger(__name__).warning(
"This Updater does not support update config files"
)
return None
def update(self):
"""
Executes the update
:return: None
"""
raise NotImplementedError()
def validate(self):
"""
Checks if the configuration is valid
:return: None
"""
if self.json_schema() is not None:
try:
validate(instance=self.config, schema=self.json_schema())
except ValidationError as e:
raise InvalidUpdateInstructions(str(e))
......@@ -17,13 +17,11 @@ You should have received a copy of the GNU General Public License
along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from enum import Enum
from toktokkie.update.XDCCUpdater import XDCCUpdater
from toktokkie.update.MangadexUpdater import MangadexUpdater
class Resolution(Enum):
"""
Enum that models the different resolution options
"""
X1080p = "1080p"
X720p = "720p"
X480p = "480p"
updaters = [MangadexUpdater, XDCCUpdater]
"""
List of all available updaters
"""
"""LICENSE
Copyright 2015 Hermann Krumrey <hermann@krumreyh.com>
This file is part of toktokkie.
toktokkie is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
toktokkie is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""