185 lines
6.3 KiB
Python
185 lines
6.3 KiB
Python
import os
|
|
from shutil import copyfile
|
|
|
|
import dicttoxml
|
|
import music_tag
|
|
from PIL import Image
|
|
from sanitize_filename import sanitize
|
|
|
|
from ytdl_subscribe import SubscriptionSource
|
|
|
|
|
|
class Subscription(object):
|
|
WORKING_DIRECTORY = ""
|
|
|
|
source = None
|
|
|
|
def __init__(self, name, options, ytdl_opts, post_process, overrides, output_path):
|
|
"""
|
|
Parameters
|
|
----------
|
|
name: str
|
|
Name of the subscription
|
|
options: dict
|
|
Dictionary of ytdl options, specific to the source type
|
|
ytdl_opts: dict
|
|
Dictionary of options passed directly to ytdl. See `youtube_dl.YoutubeDL.YoutubeDL` for options.
|
|
post_process: dict
|
|
Dictionary of ytdl-subscribe post processing options
|
|
overrides: dict
|
|
Dictionary that overrides every ytdl entry. Be careful what you override!
|
|
output_path: str
|
|
Base path to save files to
|
|
"""
|
|
self.name = name
|
|
self.options = options
|
|
self.ytdl_opts = ytdl_opts or dict()
|
|
self.post_process = post_process
|
|
self.overrides = overrides
|
|
self.output_path = output_path
|
|
|
|
# Separate each subscription's working directory
|
|
self.WORKING_DIRECTORY += f"{'/' if self.WORKING_DIRECTORY else ''}{self.name}"
|
|
|
|
# Always set outtmpl to the id and extension. Will be renamed using the subscription's output_path value
|
|
self.ytdl_opts["outtmpl"] = self.WORKING_DIRECTORY + "/%(id)s.%(ext)s"
|
|
self.ytdl_opts["writethumbnail"] = True
|
|
|
|
def format_entry(self, value, entry):
|
|
"""
|
|
Parameters
|
|
----------
|
|
value: str
|
|
String with format variables that should be populated
|
|
entry: dict
|
|
Entry used to populate any format variables in the value
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
"""
|
|
return value.format(**entry)
|
|
|
|
def format_filepath(self, filepath, entry, makedirs=False):
|
|
"""
|
|
Convert a filepath value in the config to an actual filepath.
|
|
|
|
Parameters
|
|
----------
|
|
filepath: str
|
|
File path relative to the specified output path
|
|
entry: dict
|
|
Entry used to populate any format variables in the filepath
|
|
makedirs: bool
|
|
Whether to create all directories in the final filepath.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
Full filepath.
|
|
"""
|
|
file_name = self.format_entry(filepath, entry)
|
|
output_file_path = f"{self.output_path}/{file_name}"
|
|
if makedirs:
|
|
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
|
|
|
|
return output_file_path
|
|
|
|
def parse_entry(self, entry):
|
|
# Add overrides to the entry
|
|
entry = dict(entry, **self.overrides)
|
|
|
|
# Add the file path to the entry, assert it exists
|
|
entry["file_path"] = f"{self.WORKING_DIRECTORY}/{entry['id']}.{entry['ext']}"
|
|
|
|
# Add sanitized values
|
|
entry["sanitized_artist"] = sanitize(entry["artist"])
|
|
entry["sanitized_title"] = sanitize(entry["title"])
|
|
|
|
return entry
|
|
|
|
def _post_process_tagging(self, entry):
|
|
t = music_tag.load_file(entry["file_path"])
|
|
for tag, tag_formatter in self.post_process["tagging"].items():
|
|
t[tag] = self.format_entry(tag_formatter, entry)
|
|
t.save()
|
|
|
|
def _post_process_nfo(self, entry):
|
|
nfo = {}
|
|
for tag, tag_formatter in self.post_process["nfo"].items():
|
|
nfo[tag] = self.format_entry(tag_formatter, entry)
|
|
|
|
xml = dicttoxml.dicttoxml(
|
|
obj=nfo,
|
|
root="nfo_root" in self.post_process,
|
|
custom_root=self.post_process.get("nfo_root"),
|
|
attr_type=False,
|
|
)
|
|
nfo_file_path = self.format_filepath(
|
|
self.post_process["nfo_name"], entry, makedirs=True
|
|
)
|
|
with open(nfo_file_path, "wb") as f:
|
|
f.write(xml)
|
|
|
|
def extract_info(self):
|
|
"""
|
|
Extracts only the info of the source, does not download it
|
|
"""
|
|
raise NotImplemented("Each source needs to implement how it extracts info")
|
|
|
|
def post_process_entry(self, entry):
|
|
if "tagging" in self.post_process:
|
|
self._post_process_tagging(entry)
|
|
|
|
# Move the file after all direct file modifications are complete
|
|
output_file_path = self.format_filepath(
|
|
self.post_process["file_name"], entry, makedirs=True
|
|
)
|
|
copyfile(entry["file_path"], output_file_path)
|
|
|
|
# Download the thumbnail if its present
|
|
if "thumbnail_name" in self.post_process:
|
|
thumbnail_dest_path = self.format_filepath(
|
|
self.post_process["thumbnail_name"],
|
|
entry,
|
|
makedirs=True,
|
|
)
|
|
if not os.path.isfile(thumbnail_dest_path):
|
|
thumbnail_file_path = (
|
|
f"{self.WORKING_DIRECTORY}/{entry['id']}.{entry['thumbnail_ext']}"
|
|
)
|
|
if os.path.isfile(thumbnail_file_path):
|
|
copyfile(thumbnail_file_path, thumbnail_dest_path)
|
|
|
|
if "convert_thumbnail" in self.post_process:
|
|
# TODO: Clean with yaml definitions
|
|
if self.post_process["convert_thumbnail"] == "jpg":
|
|
self.post_process["convert_thumbnail"] = "jpeg"
|
|
im = Image.open(thumbnail_dest_path).convert("RGB")
|
|
im.save(thumbnail_dest_path, self.post_process["convert_thumbnail"])
|
|
|
|
if "nfo" in self.post_process:
|
|
self._post_process_nfo(entry)
|
|
|
|
@classmethod
|
|
def from_dict(cls, name, d):
|
|
# TODO: Make sure there is only one source, verify url
|
|
if SubscriptionSource.SOUNDCLOUD in d:
|
|
from ytdl_subscribe.subscriptions.soundcloud import SoundcloudSubscription
|
|
|
|
dclass = SoundcloudSubscription
|
|
elif SubscriptionSource.YOUTUBE in d:
|
|
from ytdl_subscribe.subscriptions.youtube import YoutubeSubscription
|
|
|
|
dclass = YoutubeSubscription
|
|
else:
|
|
raise ValueError("dne")
|
|
|
|
return dclass(
|
|
name=name,
|
|
options=d[dclass.source],
|
|
ytdl_opts=d["ytdl_opts"],
|
|
post_process=d["post_process"],
|
|
overrides=d["overrides"],
|
|
output_path=d["output_path"],
|
|
)
|