diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index f9614299..f0c6e45a 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -65,8 +65,55 @@ class IndexSchedule(models.IntegerChoices): class MediaServerType(models.TextChoices): + JELLYFIN = 'j', _('Jellyfin') PLEX = 'p', _('Plex') + @classmethod + def forms_dict(cls): + from .forms import ( + JellyfinMediaServerForm, + PlexMediaServerForm, + ) + return dict(zip( + cls.values, + ( + JellyfinMediaServerForm, + PlexMediaServerForm, + ), + )) + + @classmethod + def handlers_dict(cls): + from .mediaservers import ( + JellyfinMediaServer, + PlexMediaServer, + ) + return dict(zip( + cls.values, + ( + JellyfinMediaServer, + PlexMediaServer, + ), + )) + + @property + def long_type(self): + return self.long_types().get(self.value) + + @classmethod + def long_types(cls): + d = dict(zip( + list(map(cls.lower, cls.names)), + cls.values, + )) + rd = dict(zip( d.values(), d.keys() )) + rd.update(d) + return rd + + @classmethod + def members_list(cls): + return list(cls.__members__.values()) + class MediaState(models.TextChoices): UNKNOWN = 'unknown' diff --git a/tubesync/sync/forms.py b/tubesync/sync/forms.py index c816a23b..3d795a5f 100644 --- a/tubesync/sync/forms.py +++ b/tubesync/sync/forms.py @@ -48,7 +48,39 @@ class ConfirmDeleteMediaServerForm(forms.Form): pass +_media_server_type_label = 'Jellyfin' +class JellyfinMediaServerForm(forms.Form): + host = forms.CharField( + label=_(f'Host name or IP address of the {_media_server_type_label} server'), + required=True, + ) + port = forms.IntegerField( + label=_(f'Port number of the {_media_server_type_label} server'), + required=True, + initial=8096, + ) + use_https = forms.BooleanField( + label=_('Connect over HTTPS'), + required=False, + initial=False, + ) + verify_https = forms.BooleanField( + label=_('Verify the HTTPS certificate is valid if connecting over HTTPS'), + required=False, + initial=True, + ) + token = forms.CharField( + label=_(f'{_media_server_type_label} token'), + required=True, + ) + libraries = forms.CharField( + label=_(f'Comma-separated list of {_media_server_type_label} library IDs to update'), + required=False, + ) + + +_media_server_type_label = 'Plex' class PlexMediaServerForm(forms.Form): host = forms.CharField( diff --git a/tubesync/sync/mediaservers.py b/tubesync/sync/mediaservers.py index 973d14cb..3b8e558e 100644 --- a/tubesync/sync/mediaservers.py +++ b/tubesync/sync/mediaservers.py @@ -180,11 +180,10 @@ class JellyfinMediaServer(MediaServer): 'User-Agent': 'TubeSync', 'X-Emby-Token': self.object.loaded_options['token'] # Jellyfin uses the same `X-Emby-Token` header as Emby } - base_url = f"{self.object.url}:{self.object.port}" if self.object.port else self.object.url - url = f"{base_url}{uri}" - + + url = f'{self.object.url}{uri}' log.debug(f'[jellyfin media server] Making HTTP GET request to: {url}') - + return requests.get(url, headers=headers, verify=self.object.verify_https, timeout=self.TIMEOUT) def validate(self): @@ -192,20 +191,20 @@ class JellyfinMediaServer(MediaServer): raise ValidationError('Jellyfin Media Server requires a "host"') if not self.object.port: raise ValidationError('Jellyfin Media Server requires a "port"') - + try: port = int(self.object.port) if port < 1 or port > 65535: raise ValidationError('Jellyfin Media Server "port" must be between 1 and 65535') except (TypeError, ValueError): raise ValidationError('Jellyfin Media Server "port" must be an integer') - + options = self.object.loaded_options if 'token' not in options: raise ValidationError('Jellyfin Media Server requires a "token"') if 'libraries' not in options: raise ValidationError('Jellyfin Media Server requires a "libraries"') - + # Test connection and fetch libraries try: response = self.make_request('/Library/MediaFolders', params={'Recursive': 'true', 'IncludeItemTypes': 'CollectionFolder'}) @@ -234,19 +233,18 @@ class JellyfinMediaServer(MediaServer): remote_libraries_desc.append(f'"{remote_library_name}" with ID ' f'"{remote_library_id}"') remote_libraries_str = ', '.join(remote_libraries_desc) - for library_id in libraries: - library_id = library_id.strip() + libraries = options.get('libraries', '').split(',') + for library_id in map(str.strip, libraries): if library_id not in remote_libraries: raise ValidationError(f'One or more of your specified library IDs do ' - f'not exist on your Plex Media Server. Your ' + f'not exist on your Jellyfin Media Server. Your ' f'valid libraries are: {remote_libraries_str}') - + return True def update(self): libraries = self.object.loaded_options.get('libraries', '').split(',') - for library_id in libraries: - library_id = library_id.strip() + for library_id in map(str.strip, libraries): uri = f'/Library/{library_id}/Refresh' response = self.make_request(uri) if response.status_code != 204: # 204 No Content is expected for successful refresh diff --git a/tubesync/sync/migrations/0029_alter_mediaserver_fields.py b/tubesync/sync/migrations/0029_alter_mediaserver_fields.py new file mode 100644 index 00000000..b7363430 --- /dev/null +++ b/tubesync/sync/migrations/0029_alter_mediaserver_fields.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.25 on 2025-02-22 03:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sync', '0028_alter_source_source_resolution'), + ] + + operations = [ + migrations.AlterField( + model_name='mediaserver', + name='options', + field=models.TextField(help_text='JSON encoded options for the media server', null=True, verbose_name='options'), + ), + migrations.AlterField( + model_name='mediaserver', + name='server_type', + field=models.CharField(choices=[('j', 'Jellyfin'), ('p', 'Plex')], db_index=True, default='p', help_text='Server type', max_length=1, verbose_name='server type'), + ), + migrations.AlterField( + model_name='mediaserver', + name='use_https', + field=models.BooleanField(default=False, help_text='Connect to the media server over HTTPS', verbose_name='use https'), + ), + migrations.AlterField( + model_name='mediaserver', + name='verify_https', + field=models.BooleanField(default=True, help_text='If connecting over HTTPS, verify the SSL certificate is valid', verbose_name='verify https'), + ), + ] diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index f3c051fa..cb02c2cd 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -24,7 +24,6 @@ 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 .choices import (Val, CapChoices, Fallback, FileExtension, FilterSeconds, IndexSchedule, MediaServerType, @@ -1553,11 +1552,10 @@ class MediaServer(models.Model): ''' ICONS = { + Val(MediaServerType.JELLYFIN): '', Val(MediaServerType.PLEX): '', } - HANDLERS = { - Val(MediaServerType.PLEX): PlexMediaServer, - } + HANDLERS = MediaServerType.handlers_dict() server_type = models.CharField( _('server type'), @@ -1580,17 +1578,17 @@ class MediaServer(models.Model): ) use_https = models.BooleanField( _('use https'), - default=True, + default=False, help_text=_('Connect to the media server over HTTPS') ) verify_https = models.BooleanField( _('verify https'), - default=False, + default=True, help_text=_('If connecting over HTTPS, verify the SSL certificate is valid') ) options = models.TextField( _('options'), - blank=True, + blank=False, # valid JSON only null=True, help_text=_('JSON encoded options for the media server') ) diff --git a/tubesync/sync/templates/sync/mediaserver-add.html b/tubesync/sync/templates/sync/mediaserver-add.html index 4a7c69cf..e0c4dc13 100644 --- a/tubesync/sync/templates/sync/mediaserver-add.html +++ b/tubesync/sync/templates/sync/mediaserver-add.html @@ -14,7 +14,7 @@