diff --git a/app/common/logger.py b/app/common/logger.py new file mode 100644 index 0000000..44deb26 --- /dev/null +++ b/app/common/logger.py @@ -0,0 +1,10 @@ +import logging + + +log = logging.getLogger('tubesync') +log.setLevel(logging.DEBUG) +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s [%(name)s/%(levelname)s] %(message)s') +ch.setFormatter(formatter) +log.addHandler(ch) diff --git a/app/common/static/styles/_colours.scss b/app/common/static/styles/_colours.scss index 07cb185..9033b60 100644 --- a/app/common/static/styles/_colours.scss +++ b/app/common/static/styles/_colours.scss @@ -37,6 +37,7 @@ $form-help-text-colour: $colour-light-blue; $form-delete-button-background-colour: $colour-red; $collection-no-items-text-colour: $colour-near-black; +$collection-text-colour: $colour-near-black; $collection-background-hover-colour: $colour-orange; $collection-text-hover-colour: $colour-near-white; diff --git a/app/common/static/styles/_template.scss b/app/common/static/styles/_template.scss index fc5d181..d7e8bb4 100644 --- a/app/common/static/styles/_template.scss +++ b/app/common/static/styles/_template.scss @@ -88,11 +88,12 @@ main { } .collection { + margin: 0.5rem 0 0 0 !important; .collection-item { display: block; } a.collection-item { - color: $main-link-colour; + color: $collection-text-colour; text-decoration: none; &:hover { background-color: $collection-background-hover-colour !important; diff --git a/app/sync/migrations/0010_auto_20201206_0159.py b/app/sync/migrations/0010_auto_20201206_0159.py new file mode 100644 index 0000000..87e30a9 --- /dev/null +++ b/app/sync/migrations/0010_auto_20201206_0159.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.4 on 2020-12-06 01:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sync', '0009_auto_20201205_0512'), + ] + + operations = [ + migrations.AlterField( + model_name='source', + name='directory', + field=models.CharField(db_index=True, help_text='Directory name to save the media into', max_length=100, unique=True, verbose_name='directory'), + ), + migrations.AlterField( + model_name='source', + name='fallback', + field=models.CharField(choices=[('f', 'Fail, do not download any media'), ('s', 'Get next best SD media or codec instead'), ('h', 'Get next best HD media or codec instead')], db_index=True, default='f', help_text='What do do when media in your source resolution and codecs is not available', max_length=1, verbose_name='fallback'), + ), + migrations.AlterField( + model_name='source', + name='name', + field=models.CharField(db_index=True, help_text='Friendly name for the source, used locally in TubeSync only', max_length=100, unique=True, verbose_name='name'), + ), + ] diff --git a/app/sync/models.py b/app/sync/models.py index d4c4ae9..5b46aed 100644 --- a/app/sync/models.py +++ b/app/sync/models.py @@ -128,11 +128,14 @@ class Source(models.Model): _('name'), max_length=100, db_index=True, + unique=True, help_text=_('Friendly name for the source, used locally in TubeSync only') ) directory = models.CharField( _('directory'), max_length=100, + db_index=True, + unique=True, help_text=_('Directory name to save the media into') ) delete_old_media = models.BooleanField( @@ -200,6 +203,33 @@ class Source(models.Model): def icon(self): return self.ICONS.get(self.source_type) + @property + def is_audio(self): + return self.source_resolution == self.SOURCE_RESOLUTION_AUDIO + + @property + def is_video(self): + return not self.is_audio + + @property + def extension(self): + ''' + The extension is also used by youtube-dl to set the output container. As + it is possible to quite easily pick combinations of codecs and containers + which are invalid (e.g. OPUS audio in an MP4 container) just set this for + people. All video is set to mkv containers, audio-only is set to m4a or ogg + depending on audio codec. + ''' + if self.is_audio: + if self.source_acodec == self.SOURCE_ACODEC_M4A: + return 'm4a' + elif self.source_acodec == self.SOURCE_ACODEC_OPUS: + return 'ogg' + else: + raise ValueError('Unable to choose audio extension, uknown acodec') + else: + return 'mkv' + @classmethod def create_url(obj, source_type, key): url = obj.URLS.get(source_type) @@ -387,25 +417,6 @@ class Media(models.Model): _metadata_cache[self.pk] = json.loads(self.metadata) return _metadata_cache[self.pk] - @property - def extension(self): - ''' - The extension is also used by youtube-dl to set the output container. As - it is possible to quite easily pick combinations of codecs and containers - which are invalid (e.g. OPUS audio in an MP4 container) just set this for - people. All video is set to mkv containers, audio-only is set to m4a or ogg - depending on audio codec. - ''' - if self.source.source_resolution == Source.SOURCE_RESOLUTION_AUDIO: - if self.source.source_acodec == Source.SOURCE_ACODEC_M4A: - return 'm4a' - elif self.source.source_acodec == Source.SOURCE_ACODEC_OPUS: - return 'ogg' - else: - raise ValueError('Unable to choose audio extension, uknown acodec') - else: - return 'mkv' - @property def url(self): url = self.URLS.get(self.source.source_type, '') @@ -426,6 +437,12 @@ class Media(models.Model): @property def filename(self): upload_date = self.upload_date.strftime('%Y-%m-%d') + source_name = slugify(self.source.name) title = slugify(self.title.replace('&', 'and').replace('+', 'and')) - ext = self.extension - return f'{upload_date}_{title}.{ext}' + ext = self.source.extension + fn = f'{upload_date}_{source_name}_{title}'[:100] + return f'{fn}.{ext}' + + @property + def filepath(self): + return self.source.directory_path / self.filename diff --git a/app/sync/signals.py b/app/sync/signals.py index bdbbcd3..3dfa5c4 100644 --- a/app/sync/signals.py +++ b/app/sync/signals.py @@ -1,9 +1,10 @@ from django.conf import settings -from django.db.models.signals import post_save, post_delete +from django.db.models.signals import post_save, pre_delete, post_delete from django.dispatch import receiver from .models import Source, Media from .tasks import delete_index_source_task, index_source_task, download_media_thumbnail +from .utils import delete_file @receiver(post_save, sender=Source) @@ -14,6 +15,14 @@ def source_post_save(sender, instance, created, **kwargs): index_source_task(str(instance.pk), repeat=settings.INDEX_SOURCE_EVERY) +@receiver(pre_delete, sender=Source) +def source_post_delete(sender, instance, **kwargs): + # Triggered just before a source is deleted, delete all media objects to trigger + # the Media models post_delete signal + for media in Media.objects.filter(source=instance): + media.delete() + + @receiver(post_delete, sender=Source) def source_post_delete(sender, instance, **kwargs): # Triggered when a source is deleted @@ -33,6 +42,5 @@ def media_post_save(sender, instance, created, **kwargs): @receiver(post_delete, sender=Media) def media_post_delete(sender, instance, **kwargs): - # Triggered when media is deleted - pass - # TODO: delete thumbnail and media file from disk + # Triggered when media is deleted, delete media thumbnail + delete_file(instance.thumb.path) diff --git a/app/sync/tasks.py b/app/sync/tasks.py index e51ceb1..544043a 100644 --- a/app/sync/tasks.py +++ b/app/sync/tasks.py @@ -10,6 +10,7 @@ from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from background_task import background from background_task.models import Task +from common.logger import log from .models import Source, Media from .utils import get_remote_image @@ -23,6 +24,7 @@ def delete_index_source_task(source_id): pass if task: # A scheduled task exists for this Source, delete it + log.info(f'Deleting Source index task: {task}') task.delete() @@ -51,6 +53,7 @@ def index_source_task(source_id): media.source = source media.metadata = json.dumps(video) media.save() + log.info(f'Indexed media: {source} / {media}') @background(schedule=0) @@ -68,6 +71,7 @@ def download_media_thumbnail(media_id, url): max_width, max_height = getattr(settings, 'MAX_MEDIA_THUMBNAIL_SIZE', (512, 512)) if i.width > max_width or i.height > max_height: # Image is larger than we want to save, resize it + log.info(f'Resizing thumbnail ({i.width}x{i.height}): {url}') i.thumbnail(size=(max_width, max_height)) image_file = BytesIO() i.save(image_file, 'JPEG', quality=80, optimize=True, progressive=True) @@ -81,4 +85,5 @@ def download_media_thumbnail(media_id, url): ), save=True ) + log.info(f'Saved thumbnail for: {media} from: {url}') return True diff --git a/app/sync/templates/sync/source.html b/app/sync/templates/sync/source.html index e48e8f2..2ba753d 100644 --- a/app/sync/templates/sync/source.html +++ b/app/sync/templates/sync/source.html @@ -8,6 +8,7 @@
Saving to: {{ source.directory_path }}
+