Skip to content
Commits on Source (3)
......@@ -5,7 +5,7 @@ stages:
- release
default:
image: namboy94/ci-docker-environment:0.6.0
image: namboy94/ci-docker-environment:0.8.0
before_script:
- echo "$SERVER_ACCESS_KEY" > ~/.ssh/id_rsa
- chmod 0600 ~/.ssh/id_rsa
......@@ -25,7 +25,13 @@ stylecheck:
stage: test
tags: [docker]
script:
- python-codestyle-check
- python-codestyle-check --exclude toktokkie/gui/pyuic
type_check:
stage: test
tags: [docker]
script:
- python-static-type-check
unittest:
stage: test
......
......@@ -43,7 +43,7 @@ class BookSeriesChecker(Checker):
Checks if the anilist user is up-to-date with all available volumes
:return: The check result
"""
metadata = self.metadata # type: BookSeries
metadata = self.metadata # type: BookSeries # type: ignore
manga_list = self.config["anilist_manga_list"]
anilist_id = metadata.ids.get(IdType.ANILIST, [None])[0]
......
......@@ -69,7 +69,7 @@ class MangaChecker(Checker):
:return: The result of the check
"""
# noinspection PyTypeChecker
metadata = self.metadata # type: Manga
metadata = self.metadata # type: Manga # type: ignore
anilist_entries = self.config["anilist_manga_list"]
local_chaptercount = len(os.listdir(metadata.main_path))
......
......@@ -19,7 +19,7 @@ along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
import os
from tvdb_api import tvdb_shownotfound
from typing import Dict, Optional
from typing import Dict, Optional, List
from datetime import datetime
from colorama import Fore, Style
from toktokkie.check.Checker import Checker
......@@ -68,7 +68,7 @@ class TvSeriesChecker(Checker):
:return: The result of the check
"""
valid = True
metadata = self.metadata # type: TvSeries
metadata = self.metadata # type: TvSeries # type: ignore
for season_name in os.listdir(metadata.directory_path):
season_path = os.path.join(metadata.directory_path, season_name)
......@@ -92,7 +92,7 @@ class TvSeriesChecker(Checker):
:return: The result of the check
"""
valid = True
metadata = self.metadata # type: TvSeries
metadata = self.metadata # type: TvSeries # type: ignore
ids = [metadata.tvdb_id]
for season in metadata.seasons:
......@@ -115,7 +115,7 @@ class TvSeriesChecker(Checker):
:return: The result of the check
"""
valid = True
metadata = self.metadata # type: TvSeries
metadata = self.metadata # type: TvSeries # type: ignore
ignores = self._generate_ignores_map()
tvdb_data = self.tvdb[int(metadata.tvdb_id)]
......@@ -132,8 +132,8 @@ class TvSeriesChecker(Checker):
elif episode_number in ignores.get(season_number, []):
episode_amount -= 1
existing = metadata.get_episode_files()
existing = existing[metadata.tvdb_id].get(season_number, [])
_existing = metadata.get_episode_files()
existing = _existing[metadata.tvdb_id].get(season_number, [])
if not len(existing) == episode_amount:
msg = "Mismatch in season {}; Should:{}; Is:{}".format(
......@@ -150,7 +150,7 @@ class TvSeriesChecker(Checker):
:return: The result of the check
"""
valid = True
metadata = self.metadata # type: TvSeries
metadata = self.metadata # type: TvSeries # type: ignore
ignores = self._generate_ignores_map()
tvdb_data = self.tvdb[int(metadata.tvdb_id)]
......@@ -196,7 +196,7 @@ class TvSeriesChecker(Checker):
:return: The result of the check
"""
valid = True
metadata = self.metadata # type: TvSeries
metadata = self.metadata # type: TvSeries # type: ignore
episode_files = metadata.get_episode_files()
for season in metadata.seasons:
......@@ -243,7 +243,7 @@ class TvSeriesChecker(Checker):
:return: The check result
"""
metadata = self.metadata # type: TvSeries
metadata = self.metadata # type: TvSeries # type: ignore
api = self.config["anilist_api"] # type: AnilistApi
user_list = self.config["anilist_anime_list"]
completed_ids = list(
......@@ -283,7 +283,7 @@ class TvSeriesChecker(Checker):
if anilist_id is not None:
anilist_ids.append(str(anilist_id))
resp = print(
resp = input(
"{}Set anilist IDs to {}? (y|n){}".format(
Fore.LIGHTGREEN_EX, anilist_ids, Style.RESET_ALL
)
......@@ -311,15 +311,14 @@ class TvSeriesChecker(Checker):
# check if amount of episode files is correct
# Note: Make sure to think of multi-episodes etc
def _generate_ignores_map(self) -> Dict[int, int]:
def _generate_ignores_map(self) -> Dict[int, List[int]]:
"""
Generates a dictionary mapping the excluded episode number to their
respective episodes.
:return: The generated dictionary: {season: [episodes]}
"""
metadata = self.metadata # type: TvSeries
ignores = {}
metadata = self.metadata # type: TvSeries # type: ignore
ignores = {} # type: Dict[int, List[int]]
excluded = metadata.excludes.get(IdType.TVDB, {})
multis = metadata.multi_episodes.get(IdType.TVDB, {})
......@@ -358,7 +357,7 @@ class TvSeriesChecker(Checker):
:param series_name_override: Overrides the series name
:return: The generated name
"""
metadata = self.metadata # type: TvSeries
metadata = self.metadata # type: TvSeries # type: ignore
multis = metadata.multi_episodes.get(IdType.TVDB, {})
series_name = metadata.name
......
......@@ -66,7 +66,7 @@ class VisualNovelChecker(Checker):
file.
:return: The check result
"""
metadata = self.metadata # type: VisualNovel
metadata = self.metadata # type: VisualNovel # type: ignore
valid = True
if metadata.has_op and metadata.ops is None:
......
......@@ -17,6 +17,7 @@ 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 typing import Optional
from subprocess import Popen
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QMainWindow, QWidget
......@@ -37,7 +38,7 @@ class BookWidget(QWidget, Ui_BookWidget):
"""
super().__init__(parent)
self.setupUi(self)
self.metadata = None # type: Book
self.metadata = None # type: Optional[Book]
self.initialize_buttons()
def initialize_buttons(self):
......
......@@ -17,6 +17,7 @@ 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 typing import Optional
from subprocess import Popen
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QMainWindow, QWidget
......@@ -37,7 +38,7 @@ class MovieWidget(QWidget, Ui_MovieWidget):
"""
super().__init__(parent)
self.setupUi(self)
self.metadata = None # type: Movie
self.metadata = None # type: Optional[Movie]
self.initialize_buttons()
def initialize_buttons(self):
......
......@@ -21,6 +21,7 @@ import os
import tvdb_api
import requests
import webbrowser
from typing import Optional
from subprocess import Popen
from threading import Thread
from PyQt5.QtGui import QPixmap
......@@ -43,8 +44,8 @@ class TvSeasonWidget(QWidget, Ui_TvSeasonWidget):
"""
super().__init__(parent)
self.setupUi(self)
self.metadata = None # type: TvSeries
self.season = None # type: TvSeason
self.metadata = None # type: Optional[TvSeries]
self.season = None # type: Optional[TvSeason]
self.initialize_buttons()
def initialize_buttons(self):
......
......@@ -19,6 +19,7 @@ LICENSE"""
import tvdb_api
import webbrowser
from typing import Optional
from subprocess import Popen
from threading import Thread
from PyQt5.QtGui import QPixmap
......@@ -40,7 +41,7 @@ class TvSeriesWidget(QWidget, Ui_TvSeriesWidget):
"""
super().__init__(parent)
self.setupUi(self)
self.metadata = None # type: TvSeries
self.metadata = None # type: Optional[TvSeries]
self.initialize_buttons()
def initialize_buttons(self):
......
......@@ -20,12 +20,13 @@ LICENSE"""
from typing import Type
from toktokkie.iconizing.procedures.Procedure import Procedure
from toktokkie.iconizing.procedures.GnomeProcedure import GnomeProcedure
from toktokkie.iconizing.procedures.NoopProcedure import NoopProcedure
from toktokkie.iconizing.Iconizer import Iconizer
procedures = [GnomeProcedure]
def default_procedure() -> Type[Procedure] or None:
def default_procedure() -> Type[Procedure]:
"""
Checks all available procedures for eligibility
:return: The eligible procedure or None if none were found
......@@ -33,4 +34,4 @@ def default_procedure() -> Type[Procedure] or None:
for procedure in procedures:
if procedure.is_applicable():
return procedure
return None
return NoopProcedure
......@@ -79,3 +79,6 @@ class GnomeProcedure(Procedure):
and gvfs_installed and gvfs_check
except KeyError: # pragma: no cover
return False
else:
return False
......@@ -17,32 +17,31 @@ 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 toktokkie.iconizing.procedures.Procedure import Procedure
class PromptType:
"""
Class that allows the automatic conversion of user-provided strings to
values
"""
def __init__(self, value: str):
class NoopProcedure(Procedure):
"""
Initializes the prompt type
:param value: The string value to parse
Iconizing Procedure that does nothing. Used as a fallback when no
other procedures are available
"""
self._value = value
# noinspection PyStatementEffect
self.value
@property
def value(self):
@classmethod
def iconize(cls, directory: str, icon_path_no_ext):
"""
Converts the string value into its actual value
:return: The generated value
Doesn't do anything
:param directory: The directory to iconize
:param icon_path_no_ext: The icon file without a file extension.
.png will be appended
:return: None
"""
raise NotImplementedError()
pass
def __str__(self) -> str:
@classmethod
def is_applicable(cls) -> bool:
"""
:return: A string representation of this object
Checks if this procedure is applicable to the current system.
NOOP Procedure is always applicable
:return: True if applicable, else False
"""
return str(self.value)
return True
......@@ -17,7 +17,7 @@ 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
from typing import Dict, Any
from toktokkie.metadata.Metadata import Metadata
from toktokkie.metadata.components.enums import MediaType
......@@ -35,16 +35,22 @@ class Book(Metadata):
return MediaType.BOOK
@classmethod
def prompt(cls, directory_path: str) -> Metadata:
def _prompt(cls, directory_path: str, json_data: Dict[str, Any]) \
-> Dict[str, Any]:
"""
Generates a new Metadata object using prompts for a directory
Prompts the user for metadata-type-specific information
Should be extended by child classes
:param directory_path: The path to the directory for which to generate
the metadata object
:return: The generated metadata object
"""
print("Generating metadata for {}:"
.format(os.path.basename(directory_path)))
return cls(directory_path, {
"ids": cls.prompt_for_ids(),
"type": cls.media_type().value
})
the metadata
:param json_data: Previously generated JSON data
:return: The generated metadata JSON data
"""
return {}
def _validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
:raises InvalidMetadataException: If any errors were encountered
:return: None
"""
pass
......@@ -18,7 +18,7 @@ along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import os
from typing import List
from typing import List, Dict, Any
from puffotter.os import listdir
from toktokkie.metadata.Book import Book
from toktokkie.metadata.components.BookVolume import BookVolume
......@@ -39,16 +39,16 @@ class BookSeries(Book):
return MediaType.BOOK_SERIES
@classmethod
def prompt(cls, directory_path: str) -> Book:
def _prompt(cls, directory_path: str, json_data: Dict[str, Any]) \
-> Dict[str, Any]:
"""
Generates a new Metadata object using prompts for a directory
Prompts the user for metadata-type-specific information
Should be extended by child classes
:param directory_path: The path to the directory for which to generate
the metadata object
:return: The generated metadata object
the metadata
:param json_data: Previously generated JSON data
:return: The generated metadata JSON data
"""
print("Generating metadata for {}:"
.format(os.path.basename(directory_path)))
series_ids = cls.prompt_for_ids()
series = cls(directory_path, {
"volumes": [],
......@@ -80,9 +80,9 @@ class BookSeries(Book):
})
series.volumes = volumes
return series
return series.json
@property
@property # type: ignore
@json_parameter
def volumes(self) -> List[BookVolume]:
"""
......@@ -109,3 +109,11 @@ class BookSeries(Book):
self.json["volumes"] = {}
for i, volume in enumerate(volumes):
self.json["volumes"][i] = volume.json
def _validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
:raises InvalidMetadataException: If any errors were encountered
:return: None
"""
raise NotImplementedError()
......@@ -18,11 +18,11 @@ along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import os
from typing import List
from typing import List, Dict, Any
from toktokkie.metadata.Metadata import Metadata
from toktokkie.metadata.components.enums import MediaType
from toktokkie.metadata.prompt.CommaList import CommaList
from toktokkie.metadata.helper.wrappers import json_parameter
from puffotter.prompt import prompt_comma_list
class Manga(Metadata):
......@@ -38,15 +38,16 @@ class Manga(Metadata):
return MediaType.MANGA
@classmethod
def prompt(cls, directory_path: str) -> Metadata:
def _prompt(cls, directory_path: str, json_data: Dict[str, Any]) \
-> Dict[str, Any]:
"""
Generates a new Metadata object using prompts for a directory
Prompts the user for metadata-type-specific information
Should be extended by child classes
:param directory_path: The path to the directory for which to generate
the metadata object
:return: The generated metadata object
the metadata
:param json_data: Previously generated JSON data
:return: The generated metadata JSON data
"""
print("Generating metadata for {}:"
.format(os.path.basename(directory_path)))
series = cls(directory_path, {
"ids": cls.prompt_for_ids(),
"type": cls.media_type().value,
......@@ -57,12 +58,11 @@ class Manga(Metadata):
print("Please enter identifiers for special chapters:")
for _file in sorted(os.listdir(series.special_path)):
print(_file)
series.special_chapters = cls.input(
"Special Chapters", CommaList(""), CommaList
).value
return series
series.special_chapters = prompt_comma_list("Special Chapters")
@property
return series.json
@property # type: ignore
def main_path(self) -> str:
"""
The path to the main manga directory
......@@ -70,7 +70,7 @@ class Manga(Metadata):
"""
return os.path.join(self.directory_path, "Main")
@property
@property # type: ignore
def special_path(self) -> str:
"""
The path to the special manga directory
......@@ -78,7 +78,7 @@ class Manga(Metadata):
"""
return os.path.join(self.directory_path, "Special")
@property
@property # type: ignore
@json_parameter
def special_chapters(self) -> List[str]:
"""
......@@ -96,3 +96,11 @@ class Manga(Metadata):
max_len = len(max(special_chapters, key=lambda x: len(x)))
special_chapters.sort(key=lambda x: x.zfill(max_len))
self.json["special_chapters"] = special_chapters
def _validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
:raises InvalidMetadataException: If any errors were encountered
:return: None
"""
raise NotImplementedError()
......@@ -19,17 +19,14 @@ LICENSE"""
import os
import json
from enum import Enum
from typing import List, Dict, Any, Optional
from toktokkie.metadata.helper.wrappers import json_parameter
from toktokkie.metadata.prompt.PromptType import PromptType
from toktokkie.metadata.prompt.CommaList import CommaList
from toktokkie.exceptions import InvalidMetadata, \
MissingMetadata
from toktokkie.exceptions import InvalidMetadata, MissingMetadata
from toktokkie.metadata.components.enums import MediaType, IdType
from anime_list_apis.api.AnilistApi import AnilistApi
from anime_list_apis.models.attributes.MediaType import MediaType as \
AnimeListMediaType
from puffotter.prompt import prompt_comma_list
class Metadata:
......@@ -37,6 +34,33 @@ class Metadata:
Class that acts as the base class for all possible metadata types
"""
def __init__(
self,
directory_path: str,
json_data: Optional[Dict[str, Any]] = None
):
"""
Inititalizes the metadata object using JSON data
:param directory_path: The directory of the media for which to
generate the metadata
:param json_data: Optional metadata JSON.
Will be used instead of info.json metadata
if provided
:raises InvalidMetadataException: if the metadata could not be
parsed correctly
"""
self.directory_path = directory_path
self.metadata_file = os.path.join(directory_path, ".meta/info.json")
self.icon_directory = os.path.join(directory_path, ".meta/icons")
if json_data is None:
with open(self.metadata_file, "r") as info:
self.json = json.load(info)
else:
self.json = json_data
self.validate_json()
def __str__(self) -> str:
"""
:return: A string representation of the metadata
......@@ -63,7 +87,8 @@ class Metadata:
str(self.json)
)
@property
@property # type: ignore
@json_parameter
def name(self) -> str:
"""
:return: The name of the media
......@@ -81,7 +106,7 @@ class Metadata:
os.rename(self.directory_path, new_path)
self.directory_path = new_path
@property
@property # type: ignore
@json_parameter
def tags(self) -> List[str]:
"""
......@@ -98,9 +123,9 @@ class Metadata:
"""
self.json["tags"] = tags
@property
@property # type: ignore
@json_parameter
def ids(self) -> Dict[Enum, List[str]]:
def ids(self) -> Dict[IdType, List[str]]:
"""
:return: A dictionary containing lists of IDs mapped to ID types
"""
......@@ -115,7 +140,7 @@ class Metadata:
return generated
@ids.setter
def ids(self, ids: Dict[Enum, List[str]]):
def ids(self, ids: Dict[IdType, List[str]]):
"""
Setter method for the IDs of the metadata object.
Previous IDs will be overwritten!
......@@ -145,7 +170,7 @@ class Metadata:
IdType.MYANIMELIST,
IdType.KITSU
],
MediaType.TV: [
MediaType.TV_SERIES: [
IdType.ANILIST,
IdType.KITSU,
IdType.MYANIMELIST,
......@@ -174,33 +199,6 @@ class Metadata:
]
}[cls.media_type()]
def __init__(
self,
directory_path: str,
json_data: Optional[Dict[str, Any]] = None
):
"""
Inititalizes the metadata object using JSON data
:param directory_path: The directory of the media for which to
generate the metadata
:param json_data: Optional metadata JSON.
Will be used instead of info.json metadata
if provided
:raises InvalidMetadataException: if the metadata could not be
parsed correctly
"""
self.directory_path = directory_path
self.metadata_file = os.path.join(directory_path, ".meta/info.json")
self.icon_directory = os.path.join(directory_path, ".meta/icons")
if json_data is None:
with open(self.metadata_file, "r") as info:
self.json = json.load(info)
else:
self.json = json_data
self.validate_json()
def validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
......@@ -214,6 +212,16 @@ class Metadata:
self._assert_true(len(self.ids) == len(self.json["ids"]))
self._assert_true(len(self.ids) > 0)
self._assert_true(self.media_type().value == self.json["type"])
self._validate_json()
def _validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
Should be implemented by child classes
:raises InvalidMetadataException: If any errors were encountered
:return: None
"""
raise NotImplementedError()
def write(self):
"""
......@@ -234,13 +242,46 @@ class Metadata:
))
@classmethod
def prompt(cls, directory_path: str):
def prompt(cls, directory_path: str) -> "Metadata":
"""
Generates a new Metadata object using prompts for a directory
:param directory_path: The path to the directory for which to generate
the metadata object
:return: The generated metadata object
"""
print("Generating metadata for {}:"
.format(os.path.basename(directory_path)))
idmap = {
MediaType.MANGA: [],
MediaType.BOOK: [],
MediaType.BOOK_SERIES: [],
MediaType.VISUAL_NOVEL: [IdType.VNDB],
MediaType.MOVIE: [],
MediaType.TV_SERIES: [IdType.TVDB]
} # type: Dict[MediaType, List[IdType]]
required_ids = idmap[cls.media_type()]
json_data = {
"type": cls.media_type().value,
"tags": prompt_comma_list("Tags: "),
"ids": cls.prompt_for_ids(required=required_ids)
}
json_data.update(cls._prompt(directory_path, json_data))
return cls(directory_path, json_data)
@classmethod
def _prompt(cls, directory_path: str, json_data: Dict[str, Any]) \
-> Dict[str, Any]:
"""
Prompts the user for metadata-type-specific information
Should be extended by child classes
:param directory_path: The path to the directory for which to generate
the metadata
:param json_data: Previously generated JSON data
:return: The generated metadata JSON data
"""
raise NotImplementedError()
@classmethod
......@@ -266,46 +307,11 @@ class Metadata:
except json.JSONDecodeError:
raise InvalidMetadata()
@classmethod
def input(
cls,
prompt_text: str,
default: Optional[PromptType],
_type: type(PromptType),
required: bool = False
) -> Any:
"""
Creates a user prompt that supports default options and automatic
type conversions.
:param prompt_text: The text to prompt the user
:param default: The default value to use
:param _type: The type of the prompted value
:param required: Whether or not a response is required
:return: The user's response
"""
if default is not None:
prompt_text += " {}".format(str(default))
prompt_text += ":"
response = input(prompt_text).strip()
while response == "" and default is None:
response = input(prompt_text).strip()
if response == "" and default is not None:
return default
elif response == "" and required:
return cls.input(prompt_text, default, _type, required)
else:
try:
return _type(response)
except (TypeError, ValueError):
return cls.input(prompt_text, default, _type, required)
@classmethod
def prompt_for_ids(
cls,
defaults: Optional[Dict[str, List[str]]] = None,
required: Optional[List[Enum]] = None
required: Optional[List[IdType]] = None
) -> Dict[str, List[str]]:
"""
Prompts the user for IDs
......@@ -315,17 +321,18 @@ class Metadata:
"""
required = required if required is not None else []
ids = {}
ids = {} # type: Dict[str, List[str]]
mal_updated = False
while len(ids) < 1:
for id_type in cls.valid_id_types():
default = None # type: Optional[List[str]]
if defaults is not None:
default = defaults.get(id_type.value, [])
default = CommaList(",".join(default))
else:
default = None if id_type in required else CommaList("")
elif id_type not in required:
default = []
# Load anilist ID from myanimelist ID
if IdType.MYANIMELIST.value in ids and mal_updated:
if id_type.value == IdType.ANILIST.value:
......@@ -346,41 +353,26 @@ class Metadata:
int(mal_id)
)
anilist_ids.append(str(anilist_id))
default = CommaList(",".join(anilist_ids))
prompted = cls.input(
"{} IDs".format(id_type.value),
default,
CommaList,
required=(id_type in required)
).value
default = anilist_ids
prompted = list(filter(lambda x: x != "", prompted))
min_count = 1 if id_type in required else 0
prompted = prompt_comma_list(
"{} IDs".format(id_type.value), min_count=min_count
)
if len(prompted) > 0:
ids[id_type.value] = prompted
# Check if myanimelist ID was updated
if id_type.value == IdType.MYANIMELIST.value:
if default.value is not None:
mal_updated = prompted != default.value
if default is not None:
mal_updated = prompted != default
if len(ids) < 1:
print("Please provide at least one ID")
return ids
@staticmethod
def _assert_true(condition: bool):
"""
Makes sure a statement is true by raising an exception if it's not
:param condition: The condition to check
:raises InvalidMetadataException: If the condition was False
:return: Nine
"""
if not condition:
raise InvalidMetadata()
def print_folder_icon_source(self):
"""
Prints a message with a URL for possible folder icons on deviantart
......@@ -420,3 +412,14 @@ class Metadata:
for _id in self.ids.get(IdType.ANILIST, []):
urls.append("https://anilist.co/{}/{}".format(media_type, _id))
return urls
@staticmethod
def _assert_true(condition: bool):
"""
Makes sure a statement is true by raising an exception if it's not
:param condition: The condition to check
:raises InvalidMetadataException: If the condition was False
:return: Nine
"""
if not condition:
raise InvalidMetadata()
......@@ -17,7 +17,7 @@ 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
from typing import Dict, Any
from toktokkie.metadata.Metadata import Metadata
from toktokkie.metadata.components.enums import MediaType
......@@ -35,16 +35,22 @@ class Movie(Metadata):
return MediaType.MOVIE
@classmethod
def prompt(cls, directory_path: str) -> Metadata:
def _prompt(cls, directory_path: str, json_data: Dict[str, Any]) \
-> Dict[str, Any]:
"""
Generates a new Metadata object using prompts for a directory
Prompts the user for metadata-type-specific information
Should be extended by child classes
:param directory_path: The path to the directory for which to generate
the metadata object
:return: The generated metadata object
"""
print("Generating metadata for {}:"
.format(os.path.basename(directory_path)))
return cls(directory_path, {
"ids": cls.prompt_for_ids(),
"type": cls.media_type().value
})
the metadata
:param json_data: Previously generated JSON data
:return: The generated metadata JSON data
"""
return {}
def _validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
:raises InvalidMetadataException: If any errors were encountered
:return: None
"""
pass
......@@ -19,7 +19,7 @@ LICENSE"""
import os
import tvdb_api
from typing import List, Dict
from typing import List, Dict, Any, Optional
from toktokkie.metadata.Metadata import Metadata
from toktokkie.metadata.helper.wrappers import json_parameter
from toktokkie.metadata.components.TvSeason import TvSeason
......@@ -42,21 +42,24 @@ class TvSeries(Metadata):
return MediaType.TV_SERIES
@classmethod
def prompt(cls, directory_path: str) -> Metadata:
def _prompt(cls, directory_path: str, json_data: Dict[str, Any]) \
-> Dict[str, Any]:
"""
Generates a new Metadata object using prompts for a directory
Prompts the user for metadata-type-specific information
Should be extended by child classes
:param directory_path: The path to the directory for which to generate
the metadata object
:return: The generated metadata object
the metadata
:param json_data: Previously generated JSON data
:return: The generated metadata JSON data
"""
name = os.path.basename(directory_path)
print("Generating metadata for {}:".format(name))
probable_defaults = None # type: Optional[Dict[str, List[str]]]
try:
probable_tvdb_id = str(tvdb_api.Tvdb()[name].data["id"])
probable_defaults = {IdType.TVDB.value: [probable_tvdb_id]}
except (tvdb_api.tvdb_shownotfound, TypeError):
probable_defaults = None
pass
series_ids = cls.prompt_for_ids(
defaults=probable_defaults,
......@@ -89,9 +92,9 @@ class TvSeries(Metadata):
}))
series.seasons = seasons
return series
return series.json
@property
@property # type: ignore
@json_parameter
def tvdb_id(self) -> str:
"""
......@@ -99,7 +102,7 @@ class TvSeries(Metadata):
"""
return self.ids[IdType.TVDB][0]
@property
@property # type: ignore
@json_parameter
def seasons(self) -> List[TvSeason]:
"""
......@@ -137,7 +140,7 @@ class TvSeries(Metadata):
return season
raise KeyError(season_name)
@property
@property # type: ignore
@json_parameter
def excludes(self) -> Dict[IdType, Dict[int, List[int]]]:
"""
......@@ -145,7 +148,7 @@ class TvSeries(Metadata):
:return A dictionary mapping episode info to seasons and id types
Form: {idtype: {season: [ep1, ep2]}}
"""
generated = {}
generated = {} # type: Dict[IdType, Dict[int, List[int]]]
for _id_type in self.json.get("excludes", {}):
......@@ -167,7 +170,7 @@ class TvSeries(Metadata):
return generated
@property
@property # type: ignore
@json_parameter
def season_start_overrides(self) -> Dict[IdType, Dict[int, int]]:
"""
......@@ -175,7 +178,7 @@ class TvSeries(Metadata):
point to ID types
Form: {idtype: {season: episode}}
"""
generated = {}
generated = {} # type: Dict[IdType, Dict[int, int]]
for _id_type, overrides in \
self.json.get("season_start_overrides", {}).items():
......@@ -191,14 +194,14 @@ class TvSeries(Metadata):
return generated
@property
@property # type: ignore
@json_parameter
def multi_episodes(self) -> Dict[IdType, Dict[int, Dict[int, int]]]:
"""
:return: A dictionary mapping lists of multi-episodes to id types
Form: {idtype: {season: {start: end}}}
"""
generated = {}
generated = {} # type: Dict[IdType, Dict[int, Dict[int, int]]]
for _id_type in self.json.get("multi_episodes", {}):
......@@ -268,13 +271,12 @@ class TvSeries(Metadata):
"episode": episode
})
def validate_json(self):
def _validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
:raises InvalidMetadataException: If any errors were encountered
:return: None
"""
super().validate_json()
self._assert_true("seasons" in self.json)
self._assert_true(len(self.seasons) == len(self.json["seasons"]))
self._assert_true(
......@@ -304,7 +306,7 @@ class TvSeries(Metadata):
:return: The generated dictionary. It will have the following form:
{tvdb_id: {season_number: [episode_files]}}
"""
content_info = {}
content_info = {} # type: Dict[str, Dict[int, List[str]]]
for season_name in os.listdir(self.directory_path):
......
......@@ -18,11 +18,11 @@ along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import os
from typing import Optional, List, Dict
from typing import Optional, List, Dict, Any
from puffotter.os import listdir
from toktokkie.metadata.Metadata import Metadata
from toktokkie.metadata.helper.wrappers import json_parameter
from toktokkie.metadata.components.enums import IdType, MediaType
from toktokkie.metadata.components.enums import MediaType
class VisualNovel(Metadata):
......@@ -38,21 +38,19 @@ class VisualNovel(Metadata):
return MediaType.VISUAL_NOVEL
@classmethod
def prompt(cls, directory_path: str) -> Metadata:
def _prompt(cls, directory_path: str, json_data: Dict[str, Any]) \
-> Dict[str, Any]:
"""
Generates a new Metadata object using prompts for a directory
Prompts the user for metadata-type-specific information
Should be extended by child classes
:param directory_path: The path to the directory for which to generate
the metadata object
:return: The generated metadata object
the metadata
:param json_data: Previously generated JSON data
:return: The generated metadata JSON data
"""
print("Generating metadata for {}:"
.format(os.path.basename(directory_path)))
return cls(directory_path, {
"ids": cls.prompt_for_ids(required=[IdType.VNDB]),
"type": cls.media_type().value
})
return {}
@property
@property # type: ignore
@json_parameter
def has_ed(self) -> bool:
"""
......@@ -69,7 +67,7 @@ class VisualNovel(Metadata):
"""
self.json["has_ed"] = has_ed
@property
@property # type: ignore
@json_parameter
def has_op(self) -> bool:
"""
......@@ -86,7 +84,7 @@ class VisualNovel(Metadata):
"""
self.json["has_op"] = has_op
@property
@property # type: ignore
@json_parameter
def has_cgs(self) -> bool:
"""
......@@ -103,7 +101,7 @@ class VisualNovel(Metadata):
"""
self.json["has_cgs"] = has_cgs
@property
@property # type: ignore
@json_parameter
def has_ost(self) -> bool:
"""
......@@ -187,3 +185,11 @@ class VisualNovel(Metadata):
return None
else:
return files
def _validate_json(self):
"""
Validates the JSON data to make sure everything has valid values
:raises InvalidMetadataException: If any errors were encountered
:return: None
"""
pass
......@@ -65,4 +65,4 @@ class TvSeason(MetadataPart):
"""
:return: Whether or not this season is a spinoff
"""
return self.parent.tvdb_id != self.tvdb_id
return self.parent.tvdb_id != self.tvdb_id # type: ignore