pylint spree
This commit is contained in:
parent
1243832178
commit
0545ada1f8
15 changed files with 106 additions and 35 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
[MASTER]
|
[MASTER]
|
||||||
disable=
|
disable=
|
||||||
C0114, # missing-module-docstring
|
C0114, # missing-module-docstring
|
||||||
R0903, # too-few-public-methods
|
R0903, # too-few-public-methods
|
||||||
|
R0801, # similar lines
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import tempfile
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
@ -50,17 +49,14 @@ class Downloader:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
output_directory: str,
|
working_directory: str,
|
||||||
ytdl_options: Optional[Dict] = None,
|
ytdl_options: Optional[Dict] = None,
|
||||||
download_archive_file_name: Optional[str] = None,
|
download_archive_file_name: Optional[str] = None,
|
||||||
):
|
):
|
||||||
self.output_directory = output_directory
|
self.working_directory = working_directory
|
||||||
if self.output_directory is None:
|
|
||||||
self.output_directory = tempfile.TemporaryDirectory().name
|
|
||||||
|
|
||||||
self.ytdl_options = Downloader._configure_ytdl_options(
|
self.ytdl_options = Downloader._configure_ytdl_options(
|
||||||
ytdl_options=ytdl_options,
|
ytdl_options=ytdl_options,
|
||||||
working_directory=self.output_directory,
|
working_directory=self.working_directory,
|
||||||
download_archive_file_name=download_archive_file_name,
|
download_archive_file_name=download_archive_file_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ class YoutubeDownloader(Downloader):
|
||||||
entries: List[YoutubeVideo] = []
|
entries: List[YoutubeVideo] = []
|
||||||
|
|
||||||
# Load the entries from info.json, ignore the playlist entry
|
# Load the entries from info.json, ignore the playlist entry
|
||||||
for file_name in os.listdir(self.output_directory):
|
for file_name in os.listdir(self.working_directory):
|
||||||
if file_name.endswith(".info.json") and not file_name.startswith(playlist_id):
|
if file_name.endswith(".info.json") and not file_name.startswith(playlist_id):
|
||||||
with open(Path(self.output_directory) / file_name, "r", encoding="utf-8") as file:
|
with open(Path(self.working_directory) / file_name, "r", encoding="utf-8") as file:
|
||||||
entries.append(YoutubeVideo(**json.load(file)))
|
entries.append(YoutubeVideo(**json.load(file)))
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|
@ -72,8 +72,6 @@ class YoutubeDownloader(Downloader):
|
||||||
def download_channel(self, channel_id: str) -> List[YoutubeVideo]:
|
def download_channel(self, channel_id: str) -> List[YoutubeVideo]:
|
||||||
"""
|
"""
|
||||||
Downloads all videos from a channel
|
Downloads all videos from a channel
|
||||||
TODO: Add caching via ids in the metadata. Scrape output directory for any vid ids and
|
|
||||||
TODO: include it in the archive
|
|
||||||
"""
|
"""
|
||||||
self._download_with_metadata(url=self.channel_url(channel_id))
|
self._download_with_metadata(url=self.channel_url(channel_id))
|
||||||
|
|
||||||
|
|
@ -82,9 +80,9 @@ class YoutubeDownloader(Downloader):
|
||||||
|
|
||||||
# Load the entries from info.json
|
# Load the entries from info.json
|
||||||
# TODO dupe code between this and playlist
|
# TODO dupe code between this and playlist
|
||||||
for file_name in os.listdir(self.output_directory):
|
for file_name in os.listdir(self.working_directory):
|
||||||
if file_name.endswith(".info.json") and not file_name.startswith(channel_id):
|
if file_name.endswith(".info.json") and not file_name.startswith(channel_id):
|
||||||
with open(Path(self.output_directory) / file_name, "r", encoding="utf-8") as file:
|
with open(Path(self.working_directory) / file_name, "r", encoding="utf-8") as file:
|
||||||
entries.append(YoutubeVideo(**json.load(file)))
|
entries.append(YoutubeVideo(**json.load(file)))
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,28 @@
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sanitize_filename import sanitize
|
from sanitize_filename import sanitize
|
||||||
|
|
||||||
from ytdl_subscribe.validators.base.string_formatter_validators import StringFormatterValidator
|
|
||||||
from ytdl_subscribe.validators.config.overrides.overrides_validator import OverridesValidator
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlaylistMetadata:
|
class PlaylistMetadata:
|
||||||
|
"""
|
||||||
|
Metadata for storing playlist information.
|
||||||
|
"""
|
||||||
|
|
||||||
playlist_index: int
|
playlist_index: int
|
||||||
playlist_id: str
|
playlist_id: str
|
||||||
playlist_extractor: str
|
playlist_extractor: str
|
||||||
|
|
||||||
|
|
||||||
class BaseEntry(ABC):
|
class BaseEntry(ABC):
|
||||||
|
"""
|
||||||
|
Abstract entry object to represent anything download from ytdl (playlist metadata, media, etc).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize the entry using ytdl metadata
|
Initialize the entry using ytdl metadata
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ class SoundcloudAlbumTrack(SoundcloudTrack):
|
||||||
album=album,
|
album=album,
|
||||||
album_year=album_year,
|
album_year=album_year,
|
||||||
playlist_metadata=playlist_metadata,
|
playlist_metadata=playlist_metadata,
|
||||||
**soundcloud_track._kwargs,
|
**soundcloud_track._kwargs, # pylint: disable=protected-access
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ from ytdl_subscribe.validators.config.source_options.soundcloud_validators impor
|
||||||
|
|
||||||
|
|
||||||
class SoundcloudSubscription(Subscription[SourceT], ABC):
|
class SoundcloudSubscription(Subscription[SourceT], ABC):
|
||||||
|
"""
|
||||||
|
Abstract class for all Soundcloud-based subscriptions. Sets entry type to SoundcloudTrack
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|
@ -33,6 +37,10 @@ class SoundcloudSubscription(Subscription[SourceT], ABC):
|
||||||
class SoundcloudAlbumsAndSinglesSubscription(
|
class SoundcloudAlbumsAndSinglesSubscription(
|
||||||
SoundcloudSubscription[SoundcloudAlbumsAndSinglesSourceValidator]
|
SoundcloudSubscription[SoundcloudAlbumsAndSinglesSourceValidator]
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Soundcloud subscription to download albums and tracks as singles.
|
||||||
|
"""
|
||||||
|
|
||||||
def _extract_info(self) -> List[SoundcloudTrack]:
|
def _extract_info(self) -> List[SoundcloudTrack]:
|
||||||
tracks: List[SoundcloudTrack] = []
|
tracks: List[SoundcloudTrack] = []
|
||||||
downloader = self.get_downloader(SoundcloudDownloader)
|
downloader = self.get_downloader(SoundcloudDownloader)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,19 @@ DownloaderT = TypeVar("DownloaderT", bound=Downloader)
|
||||||
|
|
||||||
|
|
||||||
class Subscription(Generic[SourceT], ABC):
|
class Subscription(Generic[SourceT], ABC):
|
||||||
|
"""
|
||||||
|
Subscription classes are the 'controllers' that perform...
|
||||||
|
|
||||||
|
- Downloading via ytdlp
|
||||||
|
- Adding metadata
|
||||||
|
- Placing files in the output directory
|
||||||
|
|
||||||
|
while configuring each step with provided configs. Child classes are expected to
|
||||||
|
provide SourceValidator (SourceT), which defines the source and its configurable options.
|
||||||
|
In addition, they should provide in the init an Entry type (EntryT), which is the entry that
|
||||||
|
will be returned after downloading.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|
@ -68,6 +81,9 @@ class Subscription(Generic[SourceT], ABC):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entry_type(self) -> EntryT:
|
def entry_type(self) -> EntryT:
|
||||||
|
"""
|
||||||
|
:return: The Entry type this subscription uses to represent downloaded media
|
||||||
|
"""
|
||||||
return self.__entry_type
|
return self.__entry_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -96,10 +112,19 @@ class Subscription(Generic[SourceT], ABC):
|
||||||
return str(Path(self.__config_options.working_directory.value) / Path(self.name))
|
return str(Path(self.__config_options.working_directory.value) / Path(self.name))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def output_directory(self):
|
def output_directory(self) -> str:
|
||||||
|
"""
|
||||||
|
:return: The formatted output directory
|
||||||
|
"""
|
||||||
return self._apply_formatter(formatter=self.output_options.output_directory)
|
return self._apply_formatter(formatter=self.output_options.output_directory)
|
||||||
|
|
||||||
def _archive_entry_file_name(self, entry: Optional[Entry], relative_file_name: str) -> None:
|
def _archive_entry_file_name(self, entry: Optional[Entry], relative_file_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Adds an entry and a file name that belongs to it into the archive mapping.
|
||||||
|
|
||||||
|
:param entry: Optional. The entry the file belongs to
|
||||||
|
:param relative_file_name: The name of the file
|
||||||
|
"""
|
||||||
if entry:
|
if entry:
|
||||||
self._enhanced_download_archive.mapping.add_entry(
|
self._enhanced_download_archive.mapping.add_entry(
|
||||||
entry=entry, entry_file_path=relative_file_name
|
entry=entry, entry_file_path=relative_file_name
|
||||||
|
|
@ -181,16 +206,18 @@ class Subscription(Generic[SourceT], ABC):
|
||||||
|
|
||||||
# Bug that mismatches webp and jpg extensions. Try to hotfix here
|
# Bug that mismatches webp and jpg extensions. Try to hotfix here
|
||||||
if not os.path.isfile(source_thumbnail_path):
|
if not os.path.isfile(source_thumbnail_path):
|
||||||
|
to_replace = f".{entry.thumbnail_ext}"
|
||||||
actual_thumbnail_ext = ".webp"
|
actual_thumbnail_ext = ".webp"
|
||||||
if entry.thumbnail_ext == "webp":
|
if entry.thumbnail_ext == "webp":
|
||||||
actual_thumbnail_ext = ".jpg"
|
actual_thumbnail_ext = ".jpg"
|
||||||
|
|
||||||
source_thumbnail_path = source_thumbnail_path.replace(
|
source_thumbnail_name = entry.download_thumbnail_name.replace(
|
||||||
f".{entry.thumbnail_ext}", actual_thumbnail_ext
|
to_replace, actual_thumbnail_ext
|
||||||
)
|
)
|
||||||
|
source_thumbnail_path = Path(self.working_directory) / source_thumbnail_name
|
||||||
|
|
||||||
if not os.path.isfile(source_thumbnail_path):
|
if not os.path.isfile(source_thumbnail_path):
|
||||||
# TODO: make more formal
|
raise ValueError("Hotfix for getting thumbnail file extension failed")
|
||||||
raise ValueError("Youtube thumbnails are a lie")
|
|
||||||
|
|
||||||
output_thumbnail_name = self._apply_formatter(
|
output_thumbnail_name = self._apply_formatter(
|
||||||
formatter=self.output_options.thumbnail_name, entry=entry
|
formatter=self.output_options.thumbnail_name, entry=entry
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
|
|
||||||
|
|
||||||
class YoutubeSubscription(Subscription[SourceT], ABC):
|
class YoutubeSubscription(Subscription[SourceT], ABC):
|
||||||
|
"""
|
||||||
|
Abstract class for all Youtube-based subscriptions. Sets entry type to YoutubeVideo
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|
@ -36,6 +40,10 @@ class YoutubeSubscription(Subscription[SourceT], ABC):
|
||||||
|
|
||||||
|
|
||||||
class YoutubePlaylistSubscription(YoutubeSubscription[YoutubePlaylistSourceValidator]):
|
class YoutubePlaylistSubscription(YoutubeSubscription[YoutubePlaylistSourceValidator]):
|
||||||
|
"""
|
||||||
|
Youtube subscription to download videos from a playlist
|
||||||
|
"""
|
||||||
|
|
||||||
def _extract_info(self) -> List[YoutubeVideo]:
|
def _extract_info(self) -> List[YoutubeVideo]:
|
||||||
return self.get_downloader(YoutubeDownloader).download_playlist(
|
return self.get_downloader(YoutubeDownloader).download_playlist(
|
||||||
playlist_id=self.source_options.playlist_id.value
|
playlist_id=self.source_options.playlist_id.value
|
||||||
|
|
@ -43,6 +51,10 @@ class YoutubePlaylistSubscription(YoutubeSubscription[YoutubePlaylistSourceValid
|
||||||
|
|
||||||
|
|
||||||
class YoutubeChannelSubscription(YoutubeSubscription[YoutubeChannelSourceValidator]):
|
class YoutubeChannelSubscription(YoutubeSubscription[YoutubeChannelSourceValidator]):
|
||||||
|
"""
|
||||||
|
Youtube subscription to download videos from a channel
|
||||||
|
"""
|
||||||
|
|
||||||
def _extract_info(self) -> List[YoutubeVideo]:
|
def _extract_info(self) -> List[YoutubeVideo]:
|
||||||
source_ytdl_options = {}
|
source_ytdl_options = {}
|
||||||
source_date_range = self.source_options.get_date_range()
|
source_date_range = self.source_options.get_date_range()
|
||||||
|
|
@ -54,6 +66,10 @@ class YoutubeChannelSubscription(YoutubeSubscription[YoutubeChannelSourceValidat
|
||||||
|
|
||||||
|
|
||||||
class YoutubeVideoSubscription(YoutubeSubscription[YoutubeVideoSourceValidator]):
|
class YoutubeVideoSubscription(YoutubeSubscription[YoutubeVideoSourceValidator]):
|
||||||
|
"""
|
||||||
|
Youtube subscription to download a single video
|
||||||
|
"""
|
||||||
|
|
||||||
def _extract_info(self) -> List[YoutubeVideo]:
|
def _extract_info(self) -> List[YoutubeVideo]:
|
||||||
entry = self.get_downloader(YoutubeDownloader).download_video(
|
entry = self.get_downloader(YoutubeDownloader).download_video(
|
||||||
video_id=self.source_options.video_id.value
|
video_id=self.source_options.video_id.value
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ class StringDatetimeValidator(Validator):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_ = datetime_from_str(self._value)
|
_ = datetime_from_str(self._value)
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
raise ValidationException(e)
|
raise ValidationException(exc) from exc
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def datetime_str(self) -> str:
|
def datetime_str(self) -> str:
|
||||||
|
|
|
||||||
|
|
@ -131,11 +131,13 @@ class DictFormatterValidator(LiteralDictValidator):
|
||||||
Validates a dictionary made up of key: string_formatters
|
Validates a dictionary made up of key: string_formatters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_key_validator = StringFormatterValidator
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
|
|
||||||
for key in self._keys:
|
for key in self._keys:
|
||||||
self._value[key] = self._validate_key(key=key, validator=StringFormatterValidator)
|
self._value[key] = self._validate_key(key=key, validator=self._key_validator)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dict(self) -> Dict[str, StringFormatterValidator]:
|
def dict(self) -> Dict[str, StringFormatterValidator]:
|
||||||
|
|
@ -146,3 +148,12 @@ class DictFormatterValidator(LiteralDictValidator):
|
||||||
def dict_with_format_strings(self) -> Dict[str, str]:
|
def dict_with_format_strings(self) -> Dict[str, str]:
|
||||||
"""Returns dict with the format strings themselves"""
|
"""Returns dict with the format strings themselves"""
|
||||||
return {key: string_formatter.format_string for key, string_formatter in self.dict.items()}
|
return {key: string_formatter.format_string for key, string_formatter in self.dict.items()}
|
||||||
|
|
||||||
|
|
||||||
|
class OverridesDictFormatterValidator(DictFormatterValidator):
|
||||||
|
"""
|
||||||
|
Validates a dictionary made up of key: string_formatters, that must be resolved by overrides
|
||||||
|
only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_key_validator = OverridesStringFormatterValidator
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
|
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
|
||||||
from ytdl_subscribe.validators.config.metadata_options.id3_validator import Id3Validator
|
from ytdl_subscribe.validators.config.metadata_options.id3_validator import Id3Validator
|
||||||
from ytdl_subscribe.validators.config.metadata_options.nfo_validator import NFOValidator
|
from ytdl_subscribe.validators.config.metadata_options.nfo_validator import NFOValidator
|
||||||
|
from ytdl_subscribe.validators.config.metadata_options.nfo_validator import (
|
||||||
|
OutputDirectoryNFOValidator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MetadataOptionsValidator(StrictDictValidator):
|
class MetadataOptionsValidator(StrictDictValidator):
|
||||||
|
|
@ -12,7 +15,6 @@ class MetadataOptionsValidator(StrictDictValidator):
|
||||||
self.id3 = self._validate_key_if_present(key="id3", validator=Id3Validator)
|
self.id3 = self._validate_key_if_present(key="id3", validator=Id3Validator)
|
||||||
self.nfo = self._validate_key_if_present(key="nfo", validator=NFOValidator)
|
self.nfo = self._validate_key_if_present(key="nfo", validator=NFOValidator)
|
||||||
|
|
||||||
# TODO: Ensure this does not depend on entry variables, only overrides
|
|
||||||
self.output_directory_nfo = self._validate_key_if_present(
|
self.output_directory_nfo = self._validate_key_if_present(
|
||||||
key="output_directory_nfo", validator=NFOValidator
|
key="output_directory_nfo", validator=OutputDirectoryNFOValidator
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,7 @@ class OutputDirectoryNFOValidator(NFOValidator):
|
||||||
self.nfo_name = self._validate_key(
|
self.nfo_name = self._validate_key(
|
||||||
key="nfo_name", validator=OverridesStringFormatterValidator
|
key="nfo_name", validator=OverridesStringFormatterValidator
|
||||||
)
|
)
|
||||||
|
self.nfo_root = self._validate_key(
|
||||||
|
key="nfo_root", validator=OverridesStringFormatterValidator
|
||||||
|
)
|
||||||
|
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator)
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,13 @@ class PresetValidator(StrictDictValidator):
|
||||||
if validator_dict is None:
|
if validator_dict is None:
|
||||||
validator_dict = self._validator_dict
|
validator_dict = self._validator_dict
|
||||||
|
|
||||||
for name, validator in validator_dict.items():
|
for validator in validator_dict.values():
|
||||||
if isinstance(validator, DictValidator):
|
if isinstance(validator, DictValidator):
|
||||||
|
# Usage of protected variables in other validators is fine. The reason to keep them
|
||||||
|
# protected is for readability when using them in subscriptions.
|
||||||
|
# pylint: disable=protected-access
|
||||||
self.__recursive_preset_validate(validator._validator_dict)
|
self.__recursive_preset_validate(validator._validator_dict)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
if isinstance(validator, OverridesStringFormatterValidator):
|
if isinstance(validator, OverridesStringFormatterValidator):
|
||||||
self.__validate_override_string_formatter_validator(validator)
|
self.__validate_override_string_formatter_validator(validator)
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,3 @@ from ytdl_subscribe.validators.base.validators import LiteralDictValidator
|
||||||
|
|
||||||
class YTDLOptionsValidator(LiteralDictValidator):
|
class YTDLOptionsValidator(LiteralDictValidator):
|
||||||
"""Ensures `ytdl_options` is a dict"""
|
"""Ensures `ytdl_options` is a dict"""
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ class DownloadArchive:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, file_path: str) -> "DownloadArchive":
|
def from_file(cls, file_path: str) -> "DownloadArchive":
|
||||||
lines = open(file_path, "r", encoding="utf8").readlines()
|
with open(file_path, "r", encoding="utf8") as file:
|
||||||
return cls.from_lines(lines=lines)
|
return cls.from_lines(lines=file.readlines())
|
||||||
|
|
||||||
def to_file(self, file_path: str) -> "DownloadArchive":
|
def to_file(self, file_path: str) -> "DownloadArchive":
|
||||||
with open(file_path, "w", encoding="utf8") as file:
|
with open(file_path, "w", encoding="utf8") as file:
|
||||||
|
|
@ -93,7 +93,9 @@ class DownloadMappings:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, json_file_path: str) -> "DownloadMappings":
|
def from_file(cls, json_file_path: str) -> "DownloadMappings":
|
||||||
entry_mappings_json = json.load(open(json_file_path, "r", encoding="utf8"))
|
with open(json_file_path, "r", encoding="utf8") as json_file:
|
||||||
|
entry_mappings_json = json.load(json_file)
|
||||||
|
|
||||||
for uid in entry_mappings_json.keys():
|
for uid in entry_mappings_json.keys():
|
||||||
entry_mappings_json[uid] = DownloadMapping.from_dict(
|
entry_mappings_json[uid] = DownloadMapping.from_dict(
|
||||||
mapping_dict=entry_mappings_json[uid]
|
mapping_dict=entry_mappings_json[uid]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue