add XML NFO file writing support, rework media cleanup deletion, resolves #11
This commit is contained in:
parent
8f4b09f346
commit
410906ad8e
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1.4 on 2020-12-19 03:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sync', '0004_source_media_format'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='source',
|
||||||
|
name='source_type',
|
||||||
|
field=models.CharField(choices=[('c', 'YouTube channel'), ('i', 'YouTube channel by ID'), ('p', 'YouTube playlist')], db_index=True, default='c', help_text='Source type', max_length=1, verbose_name='source type'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1.4 on 2020-12-19 03:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sync', '0005_auto_20201219_0312'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='source',
|
||||||
|
name='write_nfo',
|
||||||
|
field=models.BooleanField(default=False, help_text='Write an NFO file with the media, these may be detected and used by some media servers', verbose_name='write nfo'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
from xml.etree import ElementTree
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -253,6 +254,11 @@ class Source(models.Model):
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_('Copy thumbnails with the media, these may be detected and used by some media servers')
|
help_text=_('Copy thumbnails with the media, these may be detected and used by some media servers')
|
||||||
)
|
)
|
||||||
|
write_nfo = models.BooleanField(
|
||||||
|
_('write nfo'),
|
||||||
|
default=False,
|
||||||
|
help_text=_('Write an NFO file in XML with the media info, these may be detected and used by some media servers')
|
||||||
|
)
|
||||||
has_failed = models.BooleanField(
|
has_failed = models.BooleanField(
|
||||||
_('has failed'),
|
_('has failed'),
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -476,7 +482,38 @@ class Media(models.Model):
|
||||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'formats',
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'formats',
|
||||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'formats',
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'formats',
|
||||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'formats',
|
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'formats',
|
||||||
}
|
},
|
||||||
|
'categories': {
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'categories',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'categories',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'categories',
|
||||||
|
},
|
||||||
|
'rating': {
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'average_rating',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'average_rating',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'average_rating',
|
||||||
|
},
|
||||||
|
'age_limit': {
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'age_limit',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'age_limit',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'age_limit',
|
||||||
|
},
|
||||||
|
'uploader': {
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'uploader',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'uploader',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'uploader',
|
||||||
|
},
|
||||||
|
'upvotes': {
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'like_count',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'like_count',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'like_count',
|
||||||
|
},
|
||||||
|
'downvotes': {
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'dislike_count',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'dislike_count',
|
||||||
|
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'dislike_count',
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
STATE_UNKNOWN = 'unknown'
|
STATE_UNKNOWN = 'unknown'
|
||||||
STATE_SCHEDULED = 'scheduled'
|
STATE_SCHEDULED = 'scheduled'
|
||||||
|
@ -883,6 +920,34 @@ class Media(models.Model):
|
||||||
return seconds_to_timestr(duration)
|
return seconds_to_timestr(duration)
|
||||||
return '??:??:??'
|
return '??:??:??'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def categories(self):
|
||||||
|
field = self.get_metadata_field('categories')
|
||||||
|
return self.loaded_metadata.get(field, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rating(self):
|
||||||
|
field = self.get_metadata_field('rating')
|
||||||
|
return self.loaded_metadata.get(field, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def votes(self):
|
||||||
|
field = self.get_metadata_field('upvotes')
|
||||||
|
upvotes = self.loaded_metadata.get(field, 0)
|
||||||
|
field = self.get_metadata_field('downvotes')
|
||||||
|
downvotes = self.loaded_metadata.get(field, 0)
|
||||||
|
return upvotes + downvotes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def age_limit(self):
|
||||||
|
field = self.get_metadata_field('age_limit')
|
||||||
|
return self.loaded_metadata.get(field, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uploader(self):
|
||||||
|
field = self.get_metadata_field('uploader')
|
||||||
|
return self.loaded_metadata.get(field, '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def formats(self):
|
def formats(self):
|
||||||
field = self.get_metadata_field('formats')
|
field = self.get_metadata_field('formats')
|
||||||
|
@ -898,6 +963,26 @@ class Media(models.Model):
|
||||||
media_details = self.format_dict
|
media_details = self.format_dict
|
||||||
return media_format.format(**media_details)
|
return media_format.format(**media_details)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbname(self):
|
||||||
|
filename = self.filename
|
||||||
|
prefix, ext = os.path.splitext(filename)
|
||||||
|
return f'{prefix}.jpg'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbpath(self):
|
||||||
|
return self.source.directory_path / self.thumbname
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nfoname(self):
|
||||||
|
filename = self.filename
|
||||||
|
prefix, ext = os.path.splitext(filename)
|
||||||
|
return f'{prefix}.nfo'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nfopath(self):
|
||||||
|
return self.source.directory_path / self.nfoname
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def directory_path(self):
|
def directory_path(self):
|
||||||
# If a media_file has been downloaded use its existing directory
|
# If a media_file has been downloaded use its existing directory
|
||||||
|
@ -925,6 +1010,98 @@ class Media(models.Model):
|
||||||
return False
|
return False
|
||||||
return os.path.exists(self.media_file.path)
|
return os.path.exists(self.media_file.path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nfoxml(self):
|
||||||
|
'''
|
||||||
|
Returns an NFO formatted (prettified) XML string.
|
||||||
|
'''
|
||||||
|
nfo = ElementTree.Element('episodedetails')
|
||||||
|
nfo.text = '\n '
|
||||||
|
# title = media metadata title
|
||||||
|
title = nfo.makeelement('title', {})
|
||||||
|
title.text = str(self.name).strip()
|
||||||
|
title.tail = '\n '
|
||||||
|
nfo.append(title)
|
||||||
|
# showtitle = source name
|
||||||
|
showtitle = nfo.makeelement('showtitle', {})
|
||||||
|
showtitle.text = str(self.source.name).strip()
|
||||||
|
showtitle.tail = '\n '
|
||||||
|
nfo.append(showtitle)
|
||||||
|
# ratings = media metadata youtube rating
|
||||||
|
value = nfo.makeelement('value', {})
|
||||||
|
value.text = str(self.rating)
|
||||||
|
value.tail = '\n '
|
||||||
|
votes = nfo.makeelement('votes', {})
|
||||||
|
votes.text = str(self.votes)
|
||||||
|
votes.tail = '\n '
|
||||||
|
rating_attrs = {'name': 'youtube', 'max': '5', 'default': 'True'}
|
||||||
|
rating = nfo.makeelement('rating', rating_attrs)
|
||||||
|
rating.text = '\n '
|
||||||
|
rating.append(value)
|
||||||
|
rating.append(votes)
|
||||||
|
rating.tail = '\n '
|
||||||
|
ratings = nfo.makeelement('ratings', {})
|
||||||
|
ratings.text = '\n '
|
||||||
|
ratings.append(rating)
|
||||||
|
ratings.tail = '\n '
|
||||||
|
nfo.append(ratings)
|
||||||
|
# plot = media metadata description
|
||||||
|
plot = nfo.makeelement('plot', {})
|
||||||
|
plot.text = str(self.description).strip()
|
||||||
|
plot.tail = '\n '
|
||||||
|
nfo.append(plot)
|
||||||
|
# thumb = local path to media thumbnail
|
||||||
|
thumb = nfo.makeelement('thumb', {})
|
||||||
|
thumb.text = self.thumbname if self.source.copy_thumbnails else ''
|
||||||
|
thumb.tail = '\n '
|
||||||
|
nfo.append(thumb)
|
||||||
|
# mpaa = media metadata age requirement
|
||||||
|
mpaa = nfo.makeelement('mpaa', {})
|
||||||
|
mpaa.text = str(self.age_limit)
|
||||||
|
mpaa.tail = '\n '
|
||||||
|
nfo.append(mpaa)
|
||||||
|
# runtime = media metadata duration in seconds
|
||||||
|
runtime = nfo.makeelement('runtime', {})
|
||||||
|
runtime.text = str(self.duration)
|
||||||
|
runtime.tail = '\n '
|
||||||
|
nfo.append(runtime)
|
||||||
|
# id = media key
|
||||||
|
idn = nfo.makeelement('id', {})
|
||||||
|
idn.text = str(self.key).strip()
|
||||||
|
idn.tail = '\n '
|
||||||
|
nfo.append(idn)
|
||||||
|
# uniqueid = media key
|
||||||
|
uniqueid_attrs = {'type': 'youtube', 'default': 'True'}
|
||||||
|
uniqueid = nfo.makeelement('uniqueid', uniqueid_attrs)
|
||||||
|
uniqueid.text = str(self.key).strip()
|
||||||
|
uniqueid.tail = '\n '
|
||||||
|
nfo.append(uniqueid)
|
||||||
|
# studio = media metadata uploader
|
||||||
|
studio = nfo.makeelement('studio', {})
|
||||||
|
studio.text = str(self.uploader).strip()
|
||||||
|
studio.tail = '\n '
|
||||||
|
nfo.append(studio)
|
||||||
|
# aired = media metadata uploaded date
|
||||||
|
aired = nfo.makeelement('aired', {})
|
||||||
|
upload_date = self.upload_date
|
||||||
|
aired.text = upload_date.strftime('%Y-%m-%d') if upload_date else ''
|
||||||
|
aired.tail = '\n '
|
||||||
|
nfo.append(aired)
|
||||||
|
# dateadded = date and time media was created in tubesync
|
||||||
|
dateadded = nfo.makeelement('dateadded', {})
|
||||||
|
dateadded.text = self.created.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
dateadded.tail = '\n '
|
||||||
|
nfo.append(dateadded)
|
||||||
|
# genre = any media metadata categories if they exist
|
||||||
|
for category_str in self.categories:
|
||||||
|
genre = nfo.makeelement('genre', {})
|
||||||
|
genre.text = str(category_str).strip()
|
||||||
|
genre.tail = '\n '
|
||||||
|
nfo.append(genre)
|
||||||
|
nfo[-1].tail = '\n'
|
||||||
|
# Return XML tree as a prettified string
|
||||||
|
return ElementTree.tostring(nfo, encoding='utf8', method='xml').decode('utf8')
|
||||||
|
|
||||||
def get_download_state(self, task=None):
|
def get_download_state(self, task=None):
|
||||||
if self.downloaded:
|
if self.downloaded:
|
||||||
return self.STATE_DOWNLOADED
|
return self.STATE_DOWNLOADED
|
||||||
|
|
|
@ -145,20 +145,6 @@ def media_pre_delete(sender, instance, **kwargs):
|
||||||
if thumbnail_url:
|
if thumbnail_url:
|
||||||
delete_task_by_media('sync.tasks.download_media_thumbnail',
|
delete_task_by_media('sync.tasks.download_media_thumbnail',
|
||||||
(str(instance.pk), thumbnail_url))
|
(str(instance.pk), thumbnail_url))
|
||||||
# Delete media thumbnail if it exists
|
|
||||||
if instance.thumb:
|
|
||||||
log.info(f'Deleting thumbnail for: {instance} path: {instance.thumb.path}')
|
|
||||||
delete_file(instance.thumb.path)
|
|
||||||
# Delete the media file if it exists
|
|
||||||
if instance.media_file:
|
|
||||||
filepath = instance.media_file.path
|
|
||||||
log.info(f'Deleting media for: {instance} path: {filepath}')
|
|
||||||
delete_file(filepath)
|
|
||||||
# Delete thumbnail copy if it exists
|
|
||||||
barefilepath, fileext = os.path.splitext(filepath)
|
|
||||||
thumbpath = f'{barefilepath}.jpg'
|
|
||||||
log.info(f'Deleting thumbnail for: {instance} path: {thumbpath}')
|
|
||||||
delete_file(thumbpath)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Media)
|
@receiver(post_delete, sender=Media)
|
||||||
|
|
|
@ -23,7 +23,8 @@ from background_task.models import Task, CompletedTask
|
||||||
from common.logger import log
|
from common.logger import log
|
||||||
from common.errors import NoMediaException, DownloadFailedException
|
from common.errors import NoMediaException, DownloadFailedException
|
||||||
from .models import Source, Media, MediaServer
|
from .models import Source, Media, MediaServer
|
||||||
from .utils import get_remote_image, resize_image_to_height, delete_file
|
from .utils import (get_remote_image, resize_image_to_height, delete_file,
|
||||||
|
write_text_file)
|
||||||
|
|
||||||
|
|
||||||
def get_hash(task_name, pk):
|
def get_hash(task_name, pk):
|
||||||
|
@ -317,11 +318,13 @@ def download_media(media_id):
|
||||||
media.save()
|
media.save()
|
||||||
# If selected, copy the thumbnail over as well
|
# If selected, copy the thumbnail over as well
|
||||||
if media.source.copy_thumbnails and media.thumb:
|
if media.source.copy_thumbnails and media.thumb:
|
||||||
barefilepath, fileext = os.path.splitext(filepath)
|
|
||||||
thumbpath = f'{barefilepath}.jpg'
|
|
||||||
log.info(f'Copying media thumbnail from: {media.thumb.path} '
|
log.info(f'Copying media thumbnail from: {media.thumb.path} '
|
||||||
f'to: {thumbpath}')
|
f'to: {media.thumbpath}')
|
||||||
copyfile(media.thumb.path, 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: to: {media.nfopath}')
|
||||||
|
write_text_file(media.nfopath, media.nfoxml)
|
||||||
# Schedule a task to update media servers
|
# Schedule a task to update media servers
|
||||||
for mediaserver in MediaServer.objects.all():
|
for mediaserver in MediaServer.objects.all():
|
||||||
log.info(f'Scheduling media server updates')
|
log.info(f'Scheduling media server updates')
|
||||||
|
|
|
@ -97,6 +97,10 @@
|
||||||
<td class="hide-on-small-only">Copy thumbnails?</td>
|
<td class="hide-on-small-only">Copy thumbnails?</td>
|
||||||
<td><span class="hide-on-med-and-up">Copy thumbnails?<br></span><strong>{% if source.copy_thumbnails %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
|
<td><span class="hide-on-med-and-up">Copy thumbnails?<br></span><strong>{% if source.copy_thumbnails %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr title="Should an NFO file be written with the media?">
|
||||||
|
<td class="hide-on-small-only">Write NFO?</td>
|
||||||
|
<td><span class="hide-on-med-and-up">Write NFO?<br></span><strong>{% if source.write_nfo %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
|
||||||
|
</tr>
|
||||||
{% if source.delete_old_media and source.days_to_keep > 0 %}
|
{% if source.delete_old_media and source.days_to_keep > 0 %}
|
||||||
<tr title="Days after which your media from this source will be locally deleted">
|
<tr title="Days after which your media from this source will be locally deleted">
|
||||||
<td class="hide-on-small-only">Delete old media</td>
|
<td class="hide-on-small-only">Delete old media</td>
|
||||||
|
|
|
@ -3,15 +3,22 @@
|
||||||
"upload_date":"20170911",
|
"upload_date":"20170911",
|
||||||
"license":null,
|
"license":null,
|
||||||
"creator":null,
|
"creator":null,
|
||||||
"title":"no fancy stuff",
|
"title":"no fancy stuff title",
|
||||||
"alt_title":null,
|
"alt_title":null,
|
||||||
"description":"no fancy stuff",
|
"description":"no fancy stuff desc",
|
||||||
"categories":[],
|
"average_rating": 1.2345,
|
||||||
|
"dislike_count": 123,
|
||||||
|
"like_count": 456,
|
||||||
|
"uploader": "test uploader",
|
||||||
|
"categories":[
|
||||||
|
"test category 1",
|
||||||
|
"test category 2"
|
||||||
|
],
|
||||||
"tags":[],
|
"tags":[],
|
||||||
"subtitles":{},
|
"subtitles":{},
|
||||||
"automatic_captions":{},
|
"automatic_captions":{},
|
||||||
"duration":401.0,
|
"duration":401.0,
|
||||||
"age_limit":0,
|
"age_limit":50,
|
||||||
"annotations":null,
|
"annotations":null,
|
||||||
"chapters":null,
|
"chapters":null,
|
||||||
"formats":[
|
"formats":[
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
|
@ -430,7 +431,6 @@ class FrontEndTestCase(TestCase):
|
||||||
response = c.get('/tasks-completed')
|
response = c.get('/tasks-completed')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
def test_mediasevrers(self):
|
def test_mediasevrers(self):
|
||||||
# Media servers overview page
|
# Media servers overview page
|
||||||
c = Client()
|
c = Client()
|
||||||
|
@ -588,7 +588,71 @@ class FilepathTestCase(TestCase):
|
||||||
self.source.media_format = ('{title}_{key}_{resolution}-{height}x{width}-'
|
self.source.media_format = ('{title}_{key}_{resolution}-{height}x{width}-'
|
||||||
'{acodec}-{vcodec}-{fps}fps-{hdr}.{ext}')
|
'{acodec}-{vcodec}-{fps}fps-{hdr}.{ext}')
|
||||||
self.assertEqual(test_media.filename,
|
self.assertEqual(test_media.filename,
|
||||||
'no-fancy-stuff_test_720p-720x1280-opus-vp9-30fps-hdr.mkv')
|
('no-fancy-stuff-title_test_720p-720x1280-opus'
|
||||||
|
'-vp9-30fps-hdr.mkv'))
|
||||||
|
|
||||||
|
|
||||||
|
class MediaTestCase(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Disable general logging for test case
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
# Add a test source
|
||||||
|
self.source = Source.objects.create(
|
||||||
|
source_type=Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||||
|
key='testkey',
|
||||||
|
name='testname',
|
||||||
|
directory='testdirectory',
|
||||||
|
media_format=settings.MEDIA_FORMATSTR_DEFAULT,
|
||||||
|
index_schedule=3600,
|
||||||
|
delete_old_media=False,
|
||||||
|
days_to_keep=14,
|
||||||
|
source_resolution=Source.SOURCE_RESOLUTION_1080P,
|
||||||
|
source_vcodec=Source.SOURCE_VCODEC_VP9,
|
||||||
|
source_acodec=Source.SOURCE_ACODEC_OPUS,
|
||||||
|
prefer_60fps=False,
|
||||||
|
prefer_hdr=False,
|
||||||
|
fallback=Source.FALLBACK_FAIL
|
||||||
|
)
|
||||||
|
# Add some test media
|
||||||
|
self.media = Media.objects.create(
|
||||||
|
key='mediakey',
|
||||||
|
source=self.source,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
# Fix a created datetime for predictable testing
|
||||||
|
self.media.created = datetime(year=2020, month=1, day=1, hour=1,
|
||||||
|
minute=1, second=1)
|
||||||
|
|
||||||
|
def test_nfo(self):
|
||||||
|
expected_nfo = [
|
||||||
|
"<?xml version='1.0' encoding='utf8'?>",
|
||||||
|
'<episodedetails>',
|
||||||
|
' <title>no fancy stuff title</title>',
|
||||||
|
' <showtitle>testname</showtitle>',
|
||||||
|
' <ratings>',
|
||||||
|
' <rating default="True" max="5" name="youtube">',
|
||||||
|
' <value>1.2345</value>',
|
||||||
|
' <votes>579</votes>',
|
||||||
|
' </rating>',
|
||||||
|
' </ratings>',
|
||||||
|
' <plot>no fancy stuff desc</plot>',
|
||||||
|
' <thumb />', # media.thumbfile is empty without media existing
|
||||||
|
' <mpaa>50</mpaa>',
|
||||||
|
' <runtime>401</runtime>',
|
||||||
|
' <id>mediakey</id>',
|
||||||
|
' <uniqueid default="True" type="youtube">mediakey</uniqueid>',
|
||||||
|
' <studio>test uploader</studio>',
|
||||||
|
' <aired>2017-09-11</aired>',
|
||||||
|
' <dateadded>2020-01-01 01:01:01</dateadded>',
|
||||||
|
' <genre>test category 1</genre>',
|
||||||
|
' <genre>test category 2</genre>',
|
||||||
|
'</episodedetails>',
|
||||||
|
]
|
||||||
|
# Compare it line by line
|
||||||
|
test_nfo = self.media.nfoxml.split('\n')
|
||||||
|
for i, line in enumerate(test_nfo):
|
||||||
|
self.assertEqual(line, expected_nfo[i])
|
||||||
|
|
||||||
|
|
||||||
class FormatMatchingTestCase(TestCase):
|
class FormatMatchingTestCase(TestCase):
|
||||||
|
|
|
@ -108,6 +108,14 @@ def file_is_editable(filepath):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def write_text_file(filepath, filedata):
|
||||||
|
if not isinstance(filedata, str):
|
||||||
|
raise ValueError(f'filedata must be a str, got "{type(filedata)}"')
|
||||||
|
with open(filepath, 'wt') as f:
|
||||||
|
bytes_written = f.write(filedata)
|
||||||
|
return bytes_written
|
||||||
|
|
||||||
|
|
||||||
def delete_file(filepath):
|
def delete_file(filepath):
|
||||||
if file_is_editable(filepath):
|
if file_is_editable(filepath):
|
||||||
return os.remove(filepath)
|
return os.remove(filepath)
|
||||||
|
|
|
@ -276,7 +276,7 @@ class AddSourceView(CreateView):
|
||||||
fields = ('source_type', 'key', 'name', 'directory', 'media_format',
|
fields = ('source_type', 'key', 'name', 'directory', 'media_format',
|
||||||
'index_schedule', 'delete_old_media', 'days_to_keep',
|
'index_schedule', 'delete_old_media', 'days_to_keep',
|
||||||
'source_resolution', 'source_vcodec', 'source_acodec', 'prefer_60fps',
|
'source_resolution', 'source_vcodec', 'source_acodec', 'prefer_60fps',
|
||||||
'prefer_hdr', 'fallback', 'copy_thumbnails')
|
'prefer_hdr', 'fallback', 'copy_thumbnails', 'write_nfo')
|
||||||
errors = {
|
errors = {
|
||||||
'invalid_media_format': _('Invalid media format, the media format contains '
|
'invalid_media_format': _('Invalid media format, the media format contains '
|
||||||
'errors or is empty. Check the table at the end of '
|
'errors or is empty. Check the table at the end of '
|
||||||
|
@ -334,8 +334,8 @@ class SourceView(DetailView):
|
||||||
messages = {
|
messages = {
|
||||||
'source-created': _('Your new source has been created. If you have added a '
|
'source-created': _('Your new source has been created. If you have added a '
|
||||||
'very large source such as a channel with hundreds of '
|
'very large source such as a channel with hundreds of '
|
||||||
'videos it can take several minutes for media to start '
|
'videos it can take several minutes or up to an hour '
|
||||||
'to appear.'),
|
'for media to start to appear.'),
|
||||||
'source-updated': _('Your source has been updated.'),
|
'source-updated': _('Your source has been updated.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ class UpdateSourceView(UpdateView):
|
||||||
fields = ('source_type', 'key', 'name', 'directory', 'media_format',
|
fields = ('source_type', 'key', 'name', 'directory', 'media_format',
|
||||||
'index_schedule', 'delete_old_media', 'days_to_keep',
|
'index_schedule', 'delete_old_media', 'days_to_keep',
|
||||||
'source_resolution', 'source_vcodec', 'source_acodec', 'prefer_60fps',
|
'source_resolution', 'source_vcodec', 'source_acodec', 'prefer_60fps',
|
||||||
'prefer_hdr', 'fallback', 'copy_thumbnails')
|
'prefer_hdr', 'fallback', 'copy_thumbnails', 'write_nfo')
|
||||||
errors = {
|
errors = {
|
||||||
'invalid_media_format': _('Invalid media format, the media format contains '
|
'invalid_media_format': _('Invalid media format, the media format contains '
|
||||||
'errors or is empty. Check the table at the end of '
|
'errors or is empty. Check the table at the end of '
|
||||||
|
@ -411,7 +411,12 @@ class DeleteSourceView(DeleteView, FormMixin):
|
||||||
source = self.get_object()
|
source = self.get_object()
|
||||||
for media in Media.objects.filter(source=source):
|
for media in Media.objects.filter(source=source):
|
||||||
if media.media_file:
|
if media.media_file:
|
||||||
|
# Delete the media file
|
||||||
delete_file(media.media_file.name)
|
delete_file(media.media_file.name)
|
||||||
|
# Delete thumbnail copy if it exists
|
||||||
|
delete_file(media.thumbpath)
|
||||||
|
# Delete NFO file if it exists
|
||||||
|
delete_file(media.nfopath)
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -556,13 +561,12 @@ class MediaRedownloadView(FormView, SingleObjectMixin):
|
||||||
self.object.thumb = None
|
self.object.thumb = None
|
||||||
# If the media file exists on disk, delete it
|
# If the media file exists on disk, delete it
|
||||||
if self.object.media_file_exists:
|
if self.object.media_file_exists:
|
||||||
filepath = self.object.media_file.path
|
delete_file(self.object.media_file.path)
|
||||||
delete_file(filepath)
|
|
||||||
self.object.media_file = None
|
self.object.media_file = None
|
||||||
# If the media has an associated thumbnail copied, also delete it
|
# If the media has an associated thumbnail copied, also delete it
|
||||||
barefilepath, fileext = os.path.splitext(filepath)
|
delete_file(self.object.thumbpath)
|
||||||
thumbpath = f'{barefilepath}.jpg'
|
# If the media has an associated NFO file with it, also delete it
|
||||||
delete_file(thumbpath)
|
delete_file(self.object.nfopath)
|
||||||
# Reset all download data
|
# Reset all download data
|
||||||
self.object.downloaded = False
|
self.object.downloaded = False
|
||||||
self.object.downloaded_audio_codec = None
|
self.object.downloaded_audio_codec = None
|
||||||
|
@ -602,13 +606,12 @@ class MediaSkipView(FormView, SingleObjectMixin):
|
||||||
delete_task_by_media('sync.tasks.download_media', (str(self.object.pk),))
|
delete_task_by_media('sync.tasks.download_media', (str(self.object.pk),))
|
||||||
# If the media file exists on disk, delete it
|
# If the media file exists on disk, delete it
|
||||||
if self.object.media_file_exists:
|
if self.object.media_file_exists:
|
||||||
filepath = self.object.media_file.path
|
|
||||||
delete_file(self.object.media_file.path)
|
delete_file(self.object.media_file.path)
|
||||||
self.object.media_file = None
|
self.object.media_file = None
|
||||||
# If the media has an associated thumbnail copied, also delete it
|
# If the media has an associated thumbnail copied, also delete it
|
||||||
barefilepath, fileext = os.path.splitext(filepath)
|
delete_file(self.object.thumbpath)
|
||||||
thumbpath = f'{barefilepath}.jpg'
|
# If the media has an associated NFO file with it, also delete it
|
||||||
delete_file(thumbpath)
|
delete_file(self.object.nfopath)
|
||||||
# Reset all download data
|
# Reset all download data
|
||||||
self.object.downloaded = False
|
self.object.downloaded = False
|
||||||
self.object.downloaded_audio_codec = None
|
self.object.downloaded_audio_codec = None
|
||||||
|
|
Loading…
Reference in New Issue