Merge branch 'main' into issue-645-sponsorblock
This commit is contained in:
12 files changed
+252
-90
No files matched your search
@@ -1,4 +1,4 @@
|
||||
#!/command/with-contenv bash
|
||||
|
||||
exec s6-setuidgid app \
|
||||
exec nice -n "${TUBESYNC_NICE:-1}" s6-setuidgid app \
|
||||
/usr/bin/python3 /app/manage.py process_tasks
|
||||
+32
-17
@@ -3,6 +3,25 @@ from django.db import models
|
||||
from typing import Any, Optional, Dict
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
# as stolen from:
|
||||
# - https://wiki.sponsor.ajay.app/w/Types
|
||||
# - https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py
|
||||
#
|
||||
# The spacing is a little odd, it is for easy copy/paste selection.
|
||||
# Please don't change it.
|
||||
# Every possible category fits in a string < 128 characters
|
||||
class SponsorBlock_Category(models.TextChoices):
|
||||
SPONSOR = 'sponsor', _( 'Sponsor' )
|
||||
INTRO = 'intro', _( 'Intermission/Intro Animation' )
|
||||
OUTRO = 'outro', _( 'Endcards/Credits' )
|
||||
SELFPROMO = 'selfpromo', _( 'Unpaid/Self Promotion' )
|
||||
PREVIEW = 'preview', _( 'Preview/Recap' )
|
||||
FILLER = 'filler', _( 'Filler Tangent' )
|
||||
INTERACTION = 'interaction', _( 'Interaction Reminder' )
|
||||
MUSIC_OFFTOPIC = 'music_offtopic', _( 'Non-Music Section' )
|
||||
|
||||
|
||||
# this is a form field!
|
||||
class CustomCheckboxSelectMultiple(CheckboxSelectMultiple):
|
||||
template_name = 'widgets/checkbox_select.html'
|
||||
@@ -32,24 +51,28 @@ class CustomCheckboxSelectMultiple(CheckboxSelectMultiple):
|
||||
class CommaSepChoiceField(models.Field):
|
||||
"Implements comma-separated storage of lists"
|
||||
|
||||
def __init__(self, separator=",", possible_choices=(("","")), all_choice="", all_label="All", allow_all=False, *args, **kwargs):
|
||||
self.separator = separator
|
||||
# If 'text' isn't correct add the vendor override here.
|
||||
_DB_TYPES = {}
|
||||
|
||||
def __init__(self, *args, separator=",", possible_choices=(("","")), all_choice="", all_label="All", allow_all=False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.separator = str(separator)
|
||||
self.possible_choices = possible_choices
|
||||
self.selected_choices = []
|
||||
self.allow_all = allow_all
|
||||
self.all_label = all_label
|
||||
self.all_choice = all_choice
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.separator != ",":
|
||||
if ',' != self.separator:
|
||||
kwargs['separator'] = self.separator
|
||||
kwargs['possible_choices'] = self.possible_choices
|
||||
return name, path, args, kwargs
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'text'
|
||||
value = self._DB_TYPES.get(connection.vendor, None)
|
||||
return value if value is not None else 'text'
|
||||
|
||||
def get_my_choices(self):
|
||||
choiceArray = []
|
||||
@@ -60,7 +83,7 @@ class CommaSepChoiceField(models.Field):
|
||||
|
||||
for t in self.possible_choices:
|
||||
choiceArray.append(t)
|
||||
|
||||
|
||||
return choiceArray
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
@@ -72,21 +95,13 @@ class CommaSepChoiceField(models.Field):
|
||||
'label': '',
|
||||
'required': False}
|
||||
defaults.update(kwargs)
|
||||
#del defaults.required
|
||||
return super().formfield(**defaults)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
# Only include kwarg if it's not the default
|
||||
if self.separator != ",":
|
||||
kwargs['separator'] = self.separator
|
||||
return name, path, args, kwargs
|
||||
|
||||
def from_db_value(self, value, expr, conn):
|
||||
if value is None:
|
||||
if 0 == len(value) or value is None:
|
||||
self.selected_choices = []
|
||||
else:
|
||||
self.selected_choices = value.split(",")
|
||||
self.selected_choices = value.split(self.separator)
|
||||
|
||||
return self
|
||||
|
||||
@@ -97,7 +112,7 @@ class CommaSepChoiceField(models.Field):
|
||||
return ""
|
||||
|
||||
if self.all_choice not in value:
|
||||
return ",".join(value)
|
||||
return self.separator.join(value)
|
||||
else:
|
||||
return self.all_choice
|
||||
|
||||
|
||||
+97
-35
@@ -14,16 +14,18 @@ from django.core.validators import RegexValidator
|
||||
from django.utils.text import slugify
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from common.logger import log
|
||||
from common.errors import NoFormatException
|
||||
from common.utils import clean_filename, clean_emoji
|
||||
from .youtube import (get_media_info as get_youtube_media_info,
|
||||
download_media as download_youtube_media,
|
||||
get_channel_image_info as get_youtube_channel_image_info)
|
||||
from .utils import seconds_to_timestr, parse_media_format, filter_response
|
||||
from .utils import (seconds_to_timestr, parse_media_format, filter_response,
|
||||
write_text_file, mkdir_p, directory_and_stem, glob_quote)
|
||||
from .matching import (get_best_combined_format, get_best_audio_format,
|
||||
get_best_video_format)
|
||||
from .mediaservers import PlexMediaServer
|
||||
from .fields import CommaSepChoiceField
|
||||
from .fields import CommaSepChoiceField, SponsorBlock_Category
|
||||
|
||||
media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/')
|
||||
|
||||
@@ -114,21 +116,9 @@ class Source(models.Model):
|
||||
EXTENSION_MKV = 'mkv'
|
||||
EXTENSIONS = (EXTENSION_M4A, EXTENSION_OGG, EXTENSION_MKV)
|
||||
|
||||
# as stolen from: https://wiki.sponsor.ajay.app/w/Types / https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py
|
||||
SPONSORBLOCK_CATEGORIES_CHOICES = (
|
||||
('sponsor', 'Sponsor'),
|
||||
('intro', 'Intermission/Intro Animation'),
|
||||
('outro', 'Endcards/Credits'),
|
||||
('selfpromo', 'Unpaid/Self Promotion'),
|
||||
('preview', 'Preview/Recap'),
|
||||
('filler', 'Filler Tangent'),
|
||||
('interaction', 'Interaction Reminder'),
|
||||
('music_offtopic', 'Non-Music Section'),
|
||||
)
|
||||
|
||||
sponsorblock_categories = CommaSepChoiceField(
|
||||
_(''),
|
||||
possible_choices=SPONSORBLOCK_CATEGORIES_CHOICES,
|
||||
possible_choices=SponsorBlock_Category.choices,
|
||||
all_choice='all',
|
||||
allow_all=True,
|
||||
all_label='(all options)',
|
||||
@@ -537,7 +527,7 @@ class Source(models.Model):
|
||||
def get_image_url(self):
|
||||
if self.source_type == self.SOURCE_TYPE_YOUTUBE_PLAYLIST:
|
||||
raise SuspiciousOperation('This source is a playlist so it doesn\'t have thumbnail.')
|
||||
|
||||
|
||||
return get_youtube_channel_image_info(self.url)
|
||||
|
||||
|
||||
@@ -967,7 +957,7 @@ class Media(models.Model):
|
||||
|
||||
def get_best_video_format(self):
|
||||
return get_best_video_format(self)
|
||||
|
||||
|
||||
def get_format_str(self):
|
||||
'''
|
||||
Returns a youtube-dl compatible format string for the best matches
|
||||
@@ -992,7 +982,7 @@ class Media(models.Model):
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def get_display_format(self, format_str):
|
||||
'''
|
||||
Returns a tuple used in the format component of the output filename. This
|
||||
@@ -1153,9 +1143,9 @@ class Media(models.Model):
|
||||
from common.utils import json_serial
|
||||
|
||||
old_mdl = len(self.metadata or "")
|
||||
data = json.loads(self.metadata or "")
|
||||
data = json.loads(self.metadata or "{}")
|
||||
compact_json = json.dumps(data, separators=(',', ':'), default=json_serial)
|
||||
|
||||
|
||||
filtered_data = filter_response(data, True)
|
||||
filtered_json = json.dumps(filtered_data, separators=(',', ':'), default=json_serial)
|
||||
except Exception as e:
|
||||
@@ -1302,13 +1292,20 @@ class Media(models.Model):
|
||||
def filepath(self):
|
||||
return self.source.directory_path / self.filename
|
||||
|
||||
@property
|
||||
def thumbname(self):
|
||||
def filename_prefix(self):
|
||||
if self.downloaded and self.media_file:
|
||||
filename = self.media_file.path
|
||||
else:
|
||||
filename = self.filename
|
||||
# The returned prefix should not contain any directories.
|
||||
# So, we do not care about the different directories
|
||||
# used for filename in the cases above.
|
||||
prefix, ext = os.path.splitext(os.path.basename(filename))
|
||||
return prefix
|
||||
|
||||
@property
|
||||
def thumbname(self):
|
||||
prefix = self.filename_prefix()
|
||||
return f'{prefix}.jpg'
|
||||
|
||||
@property
|
||||
@@ -1317,26 +1314,18 @@ class Media(models.Model):
|
||||
|
||||
@property
|
||||
def nfoname(self):
|
||||
if self.downloaded and self.media_file:
|
||||
filename = self.media_file.path
|
||||
else:
|
||||
filename = self.filename
|
||||
prefix, ext = os.path.splitext(os.path.basename(filename))
|
||||
prefix = self.filename_prefix()
|
||||
return f'{prefix}.nfo'
|
||||
|
||||
|
||||
@property
|
||||
def nfopath(self):
|
||||
return self.directory_path / self.nfoname
|
||||
|
||||
@property
|
||||
def jsonname(self):
|
||||
if self.downloaded and self.media_file:
|
||||
filename = self.media_file.path
|
||||
else:
|
||||
filename = self.filename
|
||||
prefix, ext = os.path.splitext(os.path.basename(filename))
|
||||
prefix = self.filename_prefix()
|
||||
return f'{prefix}.info.json'
|
||||
|
||||
|
||||
@property
|
||||
def jsonpath(self):
|
||||
return self.directory_path / self.jsonname
|
||||
@@ -1564,9 +1553,82 @@ class Media(models.Model):
|
||||
|
||||
if use_padding:
|
||||
return f'{episode_number:02}'
|
||||
|
||||
|
||||
return str(episode_number)
|
||||
|
||||
def rename_files(self):
|
||||
if self.downloaded and self.media_file:
|
||||
old_video_path = Path(self.media_file.path)
|
||||
new_video_path = Path(get_media_file_path(self, None))
|
||||
if old_video_path.exists() and not new_video_path.exists():
|
||||
old_video_path = old_video_path.resolve(strict=True)
|
||||
|
||||
# move video to destination
|
||||
mkdir_p(new_video_path.parent)
|
||||
log.debug(f'{self!s}: {old_video_path!s} => {new_video_path!s}')
|
||||
old_video_path.rename(new_video_path)
|
||||
log.info(f'Renamed video file for: {self!s}')
|
||||
|
||||
# collect the list of files to move
|
||||
# this should not include the video we just moved
|
||||
(old_prefix_path, old_stem) = directory_and_stem(old_video_path)
|
||||
other_paths = list(old_prefix_path.glob(glob_quote(old_stem) + '*'))
|
||||
log.info(f'Collected {len(other_paths)} other paths for: {self!s}')
|
||||
|
||||
# adopt orphaned files, if possible
|
||||
media_format = str(self.source.media_format)
|
||||
top_dir_path = Path(self.source.directory_path)
|
||||
if '{key}' in media_format:
|
||||
fuzzy_paths = list(top_dir_path.rglob('*' + glob_quote(str(self.key)) + '*'))
|
||||
log.info(f'Collected {len(fuzzy_paths)} fuzzy paths for: {self!s}')
|
||||
|
||||
if new_video_path.exists():
|
||||
new_video_path = new_video_path.resolve(strict=True)
|
||||
|
||||
# update the media_file in the db
|
||||
self.media_file.name = str(new_video_path.relative_to(self.media_file.storage.location))
|
||||
self.save()
|
||||
log.info(f'Updated "media_file" in the database for: {self!s}')
|
||||
|
||||
(new_prefix_path, new_stem) = directory_and_stem(new_video_path)
|
||||
|
||||
# move and change names to match stem
|
||||
for other_path in other_paths:
|
||||
old_file_str = other_path.name
|
||||
new_file_str = new_stem + old_file_str[len(old_stem):]
|
||||
new_file_path = Path(new_prefix_path / new_file_str)
|
||||
log.debug(f'Considering replace for: {self!s}\n\t{other_path!s}\n\t{new_file_path!s}')
|
||||
# it should exist, but check anyway
|
||||
if other_path.exists():
|
||||
log.debug(f'{self!s}: {other_path!s} => {new_file_path!s}')
|
||||
other_path.replace(new_file_path)
|
||||
|
||||
for fuzzy_path in fuzzy_paths:
|
||||
(fuzzy_prefix_path, fuzzy_stem) = directory_and_stem(fuzzy_path)
|
||||
old_file_str = fuzzy_path.name
|
||||
new_file_str = new_stem + old_file_str[len(fuzzy_stem):]
|
||||
new_file_path = Path(new_prefix_path / new_file_str)
|
||||
log.debug(f'Considering rename for: {self!s}\n\t{fuzzy_path!s}\n\t{new_file_path!s}')
|
||||
# it quite possibly was renamed already
|
||||
if fuzzy_path.exists() and not new_file_path.exists():
|
||||
log.debug(f'{self!s}: {fuzzy_path!s} => {new_file_path!s}')
|
||||
fuzzy_path.rename(new_file_path)
|
||||
|
||||
# The thumbpath inside the .nfo file may have changed
|
||||
if self.source.write_nfo and self.source.copy_thumbnails:
|
||||
write_text_file(new_prefix_path / self.nfopath.name, self.nfoxml)
|
||||
log.info(f'Wrote new ".nfo" file for: {self!s}')
|
||||
|
||||
# try to remove empty dirs
|
||||
parent_dir = old_video_path.parent
|
||||
try:
|
||||
while parent_dir.is_dir():
|
||||
parent_dir.rmdir()
|
||||
log.info(f'Removed empty directory: {parent_dir!s}')
|
||||
parent_dir = parent_dir.parent
|
||||
except OSError as e:
|
||||
pass
|
||||
|
||||
|
||||
class MediaServer(models.Model):
|
||||
'''
|
||||
|
||||
@@ -12,7 +12,8 @@ from .tasks import (delete_task_by_source, delete_task_by_media, index_source_ta
|
||||
download_media_thumbnail, download_media_metadata,
|
||||
map_task_to_instance, check_source_directory_exists,
|
||||
download_media, rescan_media_server, download_source_images,
|
||||
save_all_media_for_source, get_media_metadata_task)
|
||||
save_all_media_for_source, rename_all_media_for_source,
|
||||
get_media_metadata_task)
|
||||
from .utils import delete_file
|
||||
from .filtering import filter_media
|
||||
|
||||
@@ -53,7 +54,7 @@ def source_post_save(sender, instance, created, **kwargs):
|
||||
if instance.source_type != Source.SOURCE_TYPE_YOUTUBE_PLAYLIST and instance.copy_channel_images:
|
||||
download_source_images(
|
||||
str(instance.pk),
|
||||
priority=0,
|
||||
priority=2,
|
||||
verbose_name=verbose_name.format(instance.name)
|
||||
)
|
||||
if instance.index_schedule > 0:
|
||||
@@ -68,10 +69,28 @@ def source_post_save(sender, instance, created, **kwargs):
|
||||
verbose_name=verbose_name.format(instance.name),
|
||||
remove_existing_tasks=True
|
||||
)
|
||||
# Check settings before any rename tasks are scheduled
|
||||
rename_sources_setting = settings.RENAME_SOURCES or list()
|
||||
create_rename_task = (
|
||||
(
|
||||
instance.directory and
|
||||
instance.directory in rename_sources_setting
|
||||
) or
|
||||
settings.RENAME_ALL_SOURCES
|
||||
)
|
||||
if create_rename_task:
|
||||
verbose_name = _('Renaming all media for source "{}"')
|
||||
rename_all_media_for_source(
|
||||
str(instance.pk),
|
||||
queue=str(instance.pk),
|
||||
priority=1,
|
||||
verbose_name=verbose_name.format(instance.name),
|
||||
remove_existing_tasks=False
|
||||
)
|
||||
verbose_name = _('Checking all media for source "{}"')
|
||||
save_all_media_for_source(
|
||||
str(instance.pk),
|
||||
priority=0,
|
||||
priority=2,
|
||||
verbose_name=verbose_name.format(instance.name),
|
||||
remove_existing_tasks=True
|
||||
)
|
||||
@@ -174,7 +193,7 @@ def media_post_save(sender, instance, created, **kwargs):
|
||||
download_media(
|
||||
str(instance.pk),
|
||||
queue=str(instance.source.pk),
|
||||
priority=15,
|
||||
priority=10,
|
||||
verbose_name=verbose_name.format(instance.name),
|
||||
remove_existing_tasks=True
|
||||
)
|
||||
|
||||
+28
-4
@@ -51,6 +51,7 @@ def map_task_to_instance(task):
|
||||
'sync.tasks.download_media': Media,
|
||||
'sync.tasks.download_media_metadata': Media,
|
||||
'sync.tasks.save_all_media_for_source': Source,
|
||||
'sync.tasks.rename_all_media_for_source': Source,
|
||||
}
|
||||
MODEL_URL_MAP = {
|
||||
Source: 'sync:source',
|
||||
@@ -442,10 +443,18 @@ def download_media(media_id):
|
||||
media.downloaded_format = 'audio'
|
||||
media.save()
|
||||
# If selected, copy the thumbnail over as well
|
||||
if media.source.copy_thumbnails and media.thumb:
|
||||
log.info(f'Copying media thumbnail from: {media.thumb.path} '
|
||||
f'to: {media.thumbpath}')
|
||||
copyfile(media.thumb.path, media.thumbpath)
|
||||
if media.source.copy_thumbnails:
|
||||
if not media.thumb_file_exists:
|
||||
thumbnail_url = media.thumbnail
|
||||
if thumbnail_url:
|
||||
args = ( str(media.pk), thumbnail_url, )
|
||||
delete_task_by_media('sync.tasks.download_media_thumbnail', args)
|
||||
if download_media_thumbnail.now(*args):
|
||||
media.refresh_from_db()
|
||||
if media.thumb_file_exists:
|
||||
log.info(f'Copying media thumbnail from: {media.thumb.path} '
|
||||
f'to: {media.thumbpath}')
|
||||
copyfile(media.thumb.path, media.thumbpath)
|
||||
# If selected, write an NFO file
|
||||
if media.source.write_nfo:
|
||||
log.info(f'Writing media NFO file to: {media.nfopath}')
|
||||
@@ -508,3 +517,18 @@ def save_all_media_for_source(source_id):
|
||||
# flags may need to be recalculated
|
||||
for media in Media.objects.filter(source=source):
|
||||
media.save()
|
||||
|
||||
|
||||
@background(schedule=0)
|
||||
def rename_all_media_for_source(source_id):
|
||||
try:
|
||||
source = Source.objects.get(pk=source_id)
|
||||
except Source.DoesNotExist:
|
||||
# Task triggered but the source no longer exists, do nothing
|
||||
log.error(f'Task rename_all_media_for_source(pk={source_id}) called but no '
|
||||
f'source exists with ID: {source_id}')
|
||||
return
|
||||
for media in Media.objects.filter(source=source):
|
||||
media.rename_files()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}{% load static %}
|
||||
{% extends 'base.html' %}{% load static %}{% load humanize %}
|
||||
|
||||
{% block headtitle %}Media - {{ media.key }}{% endblock %}
|
||||
|
||||
@@ -142,12 +142,12 @@
|
||||
<tr title="The available media formats">
|
||||
<td class="hide-on-small-only">Available formats</td>
|
||||
<td><span class="hide-on-med-and-up">Available formats<br></span>
|
||||
{% for format in media.formats %}
|
||||
{% for format in media.iter_formats %}
|
||||
<div>
|
||||
ID: <strong>{{ format.format_id }}</strong>
|
||||
{% if format.vcodec|lower != 'none' %}, {{ format.format_note }} ({{ format.width }}x{{ format.height }}), fps:{{ format.fps|lower }}, video:{{ format.vcodec }} @{{ format.tbr }}k{% endif %}
|
||||
{% if format.acodec|lower != 'none' %}, audio:{{ format.acodec }} @{{ format.abr }}k / {{ format.asr }}Hz {{ format.format_note }}{% endif %}
|
||||
{% if format.format_id == combined_format or format.format_id == audio_format or format.format_id == video_format %}<strong>(matched)</strong>{% endif %}
|
||||
ID: <strong>{{ format.id }}</strong>
|
||||
{% if format.vcodec|lower != 'none' %}, {{ format.format_note }} ({{ format.width }}x{{ format.height }}), fps:{{ format.fps|lower }}, video:{{ format.vcodec }} @{{ format.vbr }}k{% endif %}
|
||||
{% if format.acodec|lower != 'none' %}, audio:{{ format.acodec }} {% if format.abr %}@{{ format.abr }}k / {% endif %}{{ format.asr|intcomma }}Hz{% if format.language_code %} [{{ format.language_code }}]{% endif %}{% if format.abr %} {{ format.format_note }}{% endif %}{% endif %}
|
||||
{% if format.id == combined_format or format.id == audio_format or format.id == video_format %}<strong>(matched)</strong>{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
Media has no indexed available formats
|
||||
|
||||
@@ -17,7 +17,7 @@ urlpatterns = [
|
||||
path('',
|
||||
DashboardView.as_view(),
|
||||
name='dashboard'),
|
||||
|
||||
|
||||
# Source URLs
|
||||
|
||||
path('sources',
|
||||
|
||||
@@ -94,6 +94,20 @@ def resize_image_to_height(image, width, height):
|
||||
return image
|
||||
|
||||
|
||||
def glob_quote(filestr):
|
||||
_glob_specials = {
|
||||
'?': '[?]',
|
||||
'*': '[*]',
|
||||
'[': '[[]',
|
||||
']': '[]]', # probably not needed, but it won't hurt
|
||||
}
|
||||
|
||||
if not isinstance(filestr, str):
|
||||
raise TypeError(f'filestr must be a str, got "{type(filestr)}"')
|
||||
|
||||
return filestr.translate(str.maketrans(_glob_specials))
|
||||
|
||||
|
||||
def file_is_editable(filepath):
|
||||
'''
|
||||
Checks that a file exists and the file is in an allowed predefined tuple of
|
||||
@@ -115,6 +129,23 @@ def file_is_editable(filepath):
|
||||
return False
|
||||
|
||||
|
||||
def directory_and_stem(arg_path):
|
||||
filepath = Path(arg_path)
|
||||
stem = Path(filepath.stem)
|
||||
while stem.suffixes and '' != stem.suffix:
|
||||
stem = Path(stem.stem)
|
||||
stem = str(stem)
|
||||
return (filepath.parent, stem,)
|
||||
|
||||
|
||||
def mkdir_p(arg_path, mode=0o777):
|
||||
'''
|
||||
Reminder: mode only affects the last directory
|
||||
'''
|
||||
dirpath = Path(arg_path)
|
||||
return dirpath.mkdir(mode=mode, parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def write_text_file(filepath, filedata):
|
||||
if not isinstance(filedata, str):
|
||||
raise TypeError(f'filedata must be a str, got "{type(filedata)}"')
|
||||
@@ -301,11 +332,13 @@ def parse_media_format(format_dict):
|
||||
format_str = f'{height}P'
|
||||
else:
|
||||
format_str = None
|
||||
|
||||
return {
|
||||
'id': format_dict.get('format_id', ''),
|
||||
'format': format_str,
|
||||
'format_note': format_dict.get('format_note', ''),
|
||||
'format_verbose': format_dict.get('format', ''),
|
||||
'language_code': format_dict.get('language', None),
|
||||
'height': height,
|
||||
'width': width,
|
||||
'vcodec': vcodec,
|
||||
@@ -313,6 +346,7 @@ def parse_media_format(format_dict):
|
||||
'vbr': format_dict.get('tbr', 0),
|
||||
'acodec': acodec,
|
||||
'abr': format_dict.get('abr', 0),
|
||||
'asr': format_dict.get('asr', 0),
|
||||
'is_60fps': fps > 50,
|
||||
'is_hdr': 'HDR' in format_dict.get('format', '').upper(),
|
||||
'is_hls': is_hls,
|
||||
|
||||
+14
-19
@@ -112,7 +112,7 @@ class SourcesView(ListView):
|
||||
sobj = Source.objects.get(pk=kwargs["pk"])
|
||||
if sobj is None:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
verbose_name = _('Index media from source "{}" once')
|
||||
index_source_task(
|
||||
str(sobj.pk),
|
||||
@@ -354,7 +354,7 @@ class EditSourceMixin:
|
||||
obj = form.save(commit=False)
|
||||
source_type = form.cleaned_data['media_format']
|
||||
example_media_file = obj.get_example_media_format()
|
||||
|
||||
|
||||
if example_media_file == '':
|
||||
form.add_error(
|
||||
'media_format',
|
||||
@@ -524,20 +524,15 @@ class MediaView(ListView):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
q = Media.objects.all()
|
||||
|
||||
if self.filter_source:
|
||||
if self.show_skipped:
|
||||
q = Media.objects.filter(source=self.filter_source)
|
||||
elif self.only_skipped:
|
||||
q = Media.objects.filter(Q(source=self.filter_source) & (Q(skip=True) | Q(manual_skip=True)))
|
||||
else:
|
||||
q = Media.objects.filter(Q(source=self.filter_source) & (Q(skip=False) & Q(manual_skip=False)))
|
||||
else:
|
||||
if self.show_skipped:
|
||||
q = Media.objects.all()
|
||||
elif self.only_skipped:
|
||||
q = Media.objects.filter(Q(skip=True)|Q(manual_skip=True))
|
||||
else:
|
||||
q = Media.objects.filter(Q(skip=False)&Q(manual_skip=False))
|
||||
q = q.filter(source=self.filter_source)
|
||||
if self.only_skipped:
|
||||
q = q.filter(Q(can_download=False) | Q(skip=True) | Q(manual_skip=True))
|
||||
elif not self.show_skipped:
|
||||
q = q.filter(Q(can_download=True) & Q(skip=False) & Q(manual_skip=False))
|
||||
|
||||
return q.order_by('-published', '-created')
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
@@ -776,18 +771,18 @@ class MediaContent(DetailView):
|
||||
pth = pth[1]
|
||||
else:
|
||||
pth = pth[0]
|
||||
|
||||
|
||||
|
||||
|
||||
# build final path
|
||||
filepth = pathlib.Path(str(settings.DOWNLOAD_ROOT) + pth)
|
||||
|
||||
|
||||
if filepth.exists():
|
||||
# return file
|
||||
response = FileResponse(open(filepth,'rb'))
|
||||
return response
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
else:
|
||||
headers = {
|
||||
'Content-Type': self.object.content_type,
|
||||
|
||||
@@ -12,6 +12,7 @@ from copy import copy, deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from .utils import mkdir_p
|
||||
import yt_dlp
|
||||
|
||||
|
||||
@@ -24,7 +25,7 @@ _youtubedl_tempdir = getattr(settings, 'YOUTUBE_DL_TEMPDIR', None)
|
||||
if _youtubedl_tempdir:
|
||||
_youtubedl_tempdir = str(_youtubedl_tempdir)
|
||||
_youtubedl_tempdir_path = Path(_youtubedl_tempdir)
|
||||
_youtubedl_tempdir_path.mkdir(parents=True, exist_ok=True)
|
||||
mkdir_p(_youtubedl_tempdir_path)
|
||||
(_youtubedl_tempdir_path / '.ignore').touch(exist_ok=True)
|
||||
_paths = _defaults.get('paths', {})
|
||||
_paths.update({ 'temp': _youtubedl_tempdir, })
|
||||
@@ -86,7 +87,7 @@ def get_channel_image_info(url):
|
||||
with yt_dlp.YoutubeDL(opts) as y:
|
||||
try:
|
||||
response = y.extract_info(url, download=False)
|
||||
|
||||
|
||||
avatar_url = None
|
||||
banner_url = None
|
||||
for thumbnail in response['thumbnails']:
|
||||
@@ -96,7 +97,7 @@ def get_channel_image_info(url):
|
||||
banner_url = thumbnail['url']
|
||||
if banner_url != None and avatar_url != None:
|
||||
break
|
||||
|
||||
|
||||
return avatar_url, banner_url
|
||||
except yt_dlp.utils.DownloadError as e:
|
||||
raise YouTubeError(f'Failed to extract channel info for "{url}": {e}') from e
|
||||
|
||||
@@ -93,6 +93,14 @@ SHRINK_OLD_MEDIA_METADATA_STR = os.getenv('TUBESYNC_SHRINK_OLD', 'false').strip(
|
||||
SHRINK_OLD_MEDIA_METADATA = ( 'true' == SHRINK_OLD_MEDIA_METADATA_STR )
|
||||
|
||||
|
||||
# TUBESYNC_RENAME_ALL_SOURCES: True or False
|
||||
RENAME_ALL_SOURCES_STR = os.getenv('TUBESYNC_RENAME_ALL_SOURCES', 'False').strip().lower()
|
||||
RENAME_ALL_SOURCES = ( 'true' == RENAME_ALL_SOURCES_STR )
|
||||
# TUBESYNC_RENAME_SOURCES: A comma-separated list of Source directories
|
||||
RENAME_SOURCES_STR = os.getenv('TUBESYNC_RENAME_SOURCES', '')
|
||||
RENAME_SOURCES = RENAME_SOURCES_STR.split(',') if RENAME_SOURCES_STR else None
|
||||
|
||||
|
||||
VIDEO_HEIGHT_CUTOFF = int(os.getenv("TUBESYNC_VIDEO_HEIGHT_CUTOFF", "240"))
|
||||
|
||||
|
||||
|
||||
@@ -177,6 +177,10 @@ COOKIES_FILE = CONFIG_BASE_DIR / 'cookies.txt'
|
||||
MEDIA_FORMATSTR_DEFAULT = '{yyyy_mm_dd}_{source}_{title}_{key}_{format}.{ext}'
|
||||
|
||||
|
||||
RENAME_ALL_SOURCES = False
|
||||
RENAME_SOURCES = None
|
||||
|
||||
|
||||
try:
|
||||
from .local_settings import *
|
||||
except ImportError as e:
|
||||
|
||||
Reference in new issue
Block a user