316 lines
9.4 KiB
Python
316 lines
9.4 KiB
Python
import uuid
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
class Source(models.Model):
|
|
'''
|
|
A Source is a source of media. Currently, this is either a YouTube channel
|
|
or a YouTube playlist.
|
|
'''
|
|
|
|
SOURCE_TYPE_YOUTUBE_CHANNEL = 'c'
|
|
SOURCE_TYPE_YOUTUBE_PLAYLIST = 'p'
|
|
SOURCE_TYPES = (SOURCE_TYPE_YOUTUBE_CHANNEL, SOURCE_TYPE_YOUTUBE_PLAYLIST)
|
|
SOURCE_TYPE_CHOICES = (
|
|
(SOURCE_TYPE_YOUTUBE_CHANNEL, _('YouTube channel')),
|
|
(SOURCE_TYPE_YOUTUBE_PLAYLIST, _('YouTube playlist')),
|
|
)
|
|
|
|
SOURCE_PROFILE_360p = '360p'
|
|
SOURCE_PROFILE_480p = '480p'
|
|
SOURCE_PROFILE_720P = '720p'
|
|
SOURCE_PROFILE_1080P = '1080p'
|
|
SOURCE_PROFILE_2160P = '2160p'
|
|
SOURCE_PROFILE_AUDIO = 'audio'
|
|
SOURCE_PROFILES = (SOURCE_PROFILE_360p, SOURCE_PROFILE_480p, SOURCE_PROFILE_720P,
|
|
SOURCE_PROFILE_1080P, SOURCE_PROFILE_2160P,
|
|
SOURCE_PROFILE_AUDIO)
|
|
SOURCE_PROFILE_CHOICES = (
|
|
(SOURCE_PROFILE_360p, _('360p (SD)')),
|
|
(SOURCE_PROFILE_480p, _('480p (SD)')),
|
|
(SOURCE_PROFILE_720P, _('720p (HD)')),
|
|
(SOURCE_PROFILE_1080P, _('1080p (Full HD)')),
|
|
(SOURCE_PROFILE_2160P, _('2160p (4K)')),
|
|
(SOURCE_PROFILE_AUDIO, _('Audio only')),
|
|
)
|
|
|
|
OUTPUT_FORMAT_MP4 = 'mp4'
|
|
OUTPUT_FORMAT_MKV = 'mkv'
|
|
OUTPUT_FORMAT_M4A = 'm4a'
|
|
OUTPUT_FORMAT_OGG = 'ogg'
|
|
OUTPUT_FORMATS = (OUTPUT_FORMAT_MP4, OUTPUT_FORMAT_MKV, OUTPUT_FORMAT_M4A,
|
|
OUTPUT_FORMAT_OGG)
|
|
OUTPUT_FORMAT_CHOICES = (
|
|
(OUTPUT_FORMAT_MP4, _('.mp4 container')),
|
|
(OUTPUT_FORMAT_MKV, _('.mkv container')),
|
|
(OUTPUT_FORMAT_MKV, _('.webm container')),
|
|
(OUTPUT_FORMAT_M4A, _('.m4a container (audio only)')),
|
|
(OUTPUT_FORMAT_OGG, _('.ogg container (audio only)')),
|
|
)
|
|
|
|
FALLBACK_FAIL = 'f'
|
|
FALLBACK_NEXT_SD = 's'
|
|
FALLBACK_NEXT_HD = 'h'
|
|
FALLBACKS = (FALLBACK_FAIL, FALLBACK_NEXT_SD, FALLBACK_NEXT_HD)
|
|
FALLBACK_CHOICES = (
|
|
(FALLBACK_FAIL, _('Fail, do not download any media')),
|
|
(FALLBACK_NEXT_SD, _('Get next best SD media instead')),
|
|
(FALLBACK_NEXT_HD, _('Get next best HD media instead')),
|
|
)
|
|
|
|
ICONS = {
|
|
SOURCE_TYPE_YOUTUBE_CHANNEL: '<i class="fab fa-youtube"></i>',
|
|
SOURCE_TYPE_YOUTUBE_PLAYLIST: '<i class="fab fa-youtube"></i>',
|
|
}
|
|
|
|
URLS = {
|
|
SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/{key}',
|
|
SOURCE_TYPE_YOUTUBE_PLAYLIST: 'https://www.youtube.com/playlist?list={key}',
|
|
}
|
|
|
|
uuid = models.UUIDField(
|
|
_('uuid'),
|
|
primary_key=True,
|
|
editable=False,
|
|
default=uuid.uuid4,
|
|
help_text=_('UUID of the source')
|
|
)
|
|
created = models.DateTimeField(
|
|
_('created'),
|
|
auto_now_add=True,
|
|
db_index=True,
|
|
help_text=_('Date and time the source was created')
|
|
)
|
|
last_crawl = models.DateTimeField(
|
|
_('last crawl'),
|
|
db_index=True,
|
|
null=True,
|
|
blank=True,
|
|
help_text=_('Date and time the source was last crawled')
|
|
)
|
|
source_type = models.CharField(
|
|
_('source type'),
|
|
max_length=1,
|
|
db_index=True,
|
|
choices=SOURCE_TYPE_CHOICES,
|
|
default=SOURCE_TYPE_YOUTUBE_CHANNEL,
|
|
help_text=_('Source type')
|
|
)
|
|
key = models.CharField(
|
|
_('key'),
|
|
max_length=100,
|
|
db_index=True,
|
|
unique=True,
|
|
help_text=_('Source key, such as exact YouTube channel name or playlist ID')
|
|
)
|
|
name = models.CharField(
|
|
_('name'),
|
|
max_length=100,
|
|
db_index=True,
|
|
help_text=_('Friendly name for the source, used locally in TubeSync only')
|
|
)
|
|
directory = models.CharField(
|
|
_('directory'),
|
|
max_length=100,
|
|
help_text=_('Directory name to save the media into')
|
|
)
|
|
delete_old_media = models.BooleanField(
|
|
_('delete old media'),
|
|
default=False,
|
|
help_text=_('Delete old media after "days to keep" days?')
|
|
)
|
|
days_to_keep = models.PositiveSmallIntegerField(
|
|
_('days to keep'),
|
|
default=14,
|
|
help_text=_('If "delete old media" is ticked, the number of days after which '
|
|
'to automatically delete media')
|
|
)
|
|
source_profile = models.CharField(
|
|
_('source profile'),
|
|
max_length=8,
|
|
db_index=True,
|
|
choices=SOURCE_PROFILE_CHOICES,
|
|
default=SOURCE_PROFILE_1080P,
|
|
help_text=_('Source profile, the quality to attempt to download media')
|
|
)
|
|
prefer_60fps = models.BooleanField(
|
|
_('prefer 60fps'),
|
|
default=False,
|
|
help_text=_('Where possible, prefer 60fps media for this source')
|
|
)
|
|
prefer_hdr = models.BooleanField(
|
|
_('prefer hdr'),
|
|
default=False,
|
|
help_text=_('Where possible, prefer HDR media for this source')
|
|
)
|
|
output_format = models.CharField(
|
|
_('output format'),
|
|
max_length=8,
|
|
db_index=True,
|
|
choices=OUTPUT_FORMAT_CHOICES,
|
|
default=OUTPUT_FORMAT_MKV,
|
|
help_text=_('Output format, the file format container in which to save media')
|
|
)
|
|
fallback = models.CharField(
|
|
_('fallback'),
|
|
max_length=1,
|
|
db_index=True,
|
|
choices=FALLBACK_CHOICES,
|
|
default=FALLBACK_FAIL,
|
|
help_text=_('What do do when media in your source profile is not available')
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _('Source')
|
|
verbose_name_plural = _('Sources')
|
|
|
|
@property
|
|
def icon(self):
|
|
return self.ICONS.get(self.source_type)
|
|
|
|
@property
|
|
def url(self):
|
|
url = self.URLS.get(self.source_type)
|
|
return url.format(key=self.key)
|
|
|
|
@property
|
|
def directory_path(self):
|
|
if self.source_profile == self.SOURCE_PROFILE_AUDIO:
|
|
return settings.SYNC_AUDIO_ROOT / self.directory
|
|
else:
|
|
return settings.SYNC_VIDEO_ROOT / self.directory
|
|
|
|
|
|
def get_media_thumb_path(instance, filename):
|
|
fileid = str(instance.uuid)
|
|
filename = f'{fileid.lower()}.{instance.image_type.lower()}'
|
|
prefix = fileid[:2]
|
|
return os.path.join('thumbs', prefix, filename)
|
|
|
|
|
|
class Media(models.Model):
|
|
'''
|
|
Media is a single piece of media, such as a single YouTube video linked to a
|
|
Source.
|
|
'''
|
|
|
|
uuid = models.UUIDField(
|
|
_('uuid'),
|
|
primary_key=True,
|
|
editable=False,
|
|
default=uuid.uuid4,
|
|
help_text=_('UUID of the media')
|
|
)
|
|
created = models.DateTimeField(
|
|
_('created'),
|
|
auto_now_add=True,
|
|
db_index=True,
|
|
help_text=_('Date and time the media was created')
|
|
)
|
|
source = models.ForeignKey(
|
|
Source,
|
|
on_delete=models.CASCADE,
|
|
related_name='media_source',
|
|
help_text=_('Source the media belongs to')
|
|
)
|
|
published = models.DateTimeField(
|
|
_('published'),
|
|
db_index=True,
|
|
null=True,
|
|
blank=True,
|
|
help_text=_('Date and time the media was published on the source')
|
|
)
|
|
key = models.CharField(
|
|
_('key'),
|
|
max_length=100,
|
|
db_index=True,
|
|
help_text=_('Media key, such as exact YouTube video ID')
|
|
)
|
|
thumb = models.ImageField(
|
|
_('thumb'),
|
|
upload_to=get_media_thumb_path,
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
width_field='thumb_width',
|
|
height_field='thumb_height',
|
|
help_text=_('Thumbnail')
|
|
)
|
|
thumb_width = models.PositiveSmallIntegerField(
|
|
_('thumb width'),
|
|
blank=True,
|
|
help_text=_('Width (X) of the thumbnail')
|
|
)
|
|
thumb_height = models.PositiveSmallIntegerField(
|
|
_('thumb height'),
|
|
blank=True,
|
|
help_text=_('Height (Y) of the thumbnail')
|
|
)
|
|
metadata = models.TextField(
|
|
_('metadata'),
|
|
blank=True,
|
|
null=True,
|
|
help_text=_('JSON encoded metadata for the media')
|
|
)
|
|
downloaded = models.BooleanField(
|
|
_('downloaded'),
|
|
db_index=True,
|
|
default=False,
|
|
help_text=_('Media has been downloaded')
|
|
)
|
|
downloaded_audio_codec = models.CharField(
|
|
_('downloaded audio codec'),
|
|
max_length=30,
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_('Audio codec of the downloaded media')
|
|
)
|
|
downloaded_video_codec = models.CharField(
|
|
_('downloaded video codec'),
|
|
max_length=30,
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_('Video codec of the downloaded media')
|
|
)
|
|
downloaded_container = models.CharField(
|
|
_('downloaded container format'),
|
|
max_length=30,
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_('Container format of the downloaded media')
|
|
)
|
|
downloaded_fps = models.PositiveSmallIntegerField(
|
|
_('downloaded fps'),
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_('FPS of the downloaded media')
|
|
)
|
|
downloaded_hdr = models.BooleanField(
|
|
_('downloaded hdr'),
|
|
default=False,
|
|
help_text=_('Downloaded media has HDR')
|
|
)
|
|
downloaded_filesize = models.PositiveBigIntegerField(
|
|
_('downloaded filesize'),
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_('Size of the downloaded media in bytes')
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.key
|
|
|
|
class Meta:
|
|
verbose_name = _('Media')
|
|
verbose_name_plural = _('Media')
|