Connect the UI for Jellyfin

* Add Jellyfin media server type

* Add and use `forms_dict` and `long_types` methods

* Add `JellyfinMediaServerForm`

* Use `JellyfinMediaServer`

* Add and use `MediaServerType.handlers_dict` method

* Do not hard-code `plex`

* Also map backwards in `MediaServerType.long_types`

* Use the long server type for URLs

* Loop over media server type names

* Add `MediaServerType.long_type` property

* Add `media_server_types` to template context

* Use the `MediaServerType.members_list` class method

* Port is already part of the URL
This commit is contained in:
tcely authored and GitHub committed 2025-02-22 05:49:03 -05:00
1 parent e25a9cd228
commit 976101781d
8 files changed
+143 -34

No files matched your search

+47
View File
@@ -65,8 +65,55 @@ class IndexSchedule(models.IntegerChoices):
class MediaServerType(models.TextChoices): class MediaServerType(models.TextChoices):
JELLYFIN = 'j', _('Jellyfin')
PLEX = 'p', _('Plex') 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): class MediaState(models.TextChoices):
UNKNOWN = 'unknown' UNKNOWN = 'unknown'
+32
View File
@@ -48,7 +48,39 @@ class ConfirmDeleteMediaServerForm(forms.Form):
pass 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): class PlexMediaServerForm(forms.Form):
host = forms.CharField( host = forms.CharField(
+11 -13
View File
@@ -180,11 +180,10 @@ class JellyfinMediaServer(MediaServer):
'User-Agent': 'TubeSync', 'User-Agent': 'TubeSync',
'X-Emby-Token': self.object.loaded_options['token'] # Jellyfin uses the same `X-Emby-Token` header as Emby '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}') 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) return requests.get(url, headers=headers, verify=self.object.verify_https, timeout=self.TIMEOUT)
def validate(self): def validate(self):
@@ -192,20 +191,20 @@ class JellyfinMediaServer(MediaServer):
raise ValidationError('Jellyfin Media Server requires a "host"') raise ValidationError('Jellyfin Media Server requires a "host"')
if not self.object.port: if not self.object.port:
raise ValidationError('Jellyfin Media Server requires a "port"') raise ValidationError('Jellyfin Media Server requires a "port"')
try: try:
port = int(self.object.port) port = int(self.object.port)
if port < 1 or port > 65535: if port < 1 or port > 65535:
raise ValidationError('Jellyfin Media Server "port" must be between 1 and 65535') raise ValidationError('Jellyfin Media Server "port" must be between 1 and 65535')
except (TypeError, ValueError): except (TypeError, ValueError):
raise ValidationError('Jellyfin Media Server "port" must be an integer') raise ValidationError('Jellyfin Media Server "port" must be an integer')
options = self.object.loaded_options options = self.object.loaded_options
if 'token' not in options: if 'token' not in options:
raise ValidationError('Jellyfin Media Server requires a "token"') raise ValidationError('Jellyfin Media Server requires a "token"')
if 'libraries' not in options: if 'libraries' not in options:
raise ValidationError('Jellyfin Media Server requires a "libraries"') raise ValidationError('Jellyfin Media Server requires a "libraries"')
# Test connection and fetch libraries # Test connection and fetch libraries
try: try:
response = self.make_request('/Library/MediaFolders', params={'Recursive': 'true', 'IncludeItemTypes': 'CollectionFolder'}) 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 ' remote_libraries_desc.append(f'"{remote_library_name}" with ID '
f'"{remote_library_id}"') f'"{remote_library_id}"')
remote_libraries_str = ', '.join(remote_libraries_desc) remote_libraries_str = ', '.join(remote_libraries_desc)
for library_id in libraries: libraries = options.get('libraries', '').split(',')
library_id = library_id.strip() for library_id in map(str.strip, libraries):
if library_id not in remote_libraries: if library_id not in remote_libraries:
raise ValidationError(f'One or more of your specified library IDs do ' 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}') f'valid libraries are: {remote_libraries_str}')
return True return True
def update(self): def update(self):
libraries = self.object.loaded_options.get('libraries', '').split(',') libraries = self.object.loaded_options.get('libraries', '').split(',')
for library_id in libraries: for library_id in map(str.strip, libraries):
library_id = library_id.strip()
uri = f'/Library/{library_id}/Refresh' uri = f'/Library/{library_id}/Refresh'
response = self.make_request(uri) response = self.make_request(uri)
if response.status_code != 204: # 204 No Content is expected for successful refresh if response.status_code != 204: # 204 No Content is expected for successful refresh
@@ -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'),
),
]
+5 -7
View File
@@ -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) write_text_file, mkdir_p, directory_and_stem, glob_quote)
from .matching import (get_best_combined_format, get_best_audio_format, from .matching import (get_best_combined_format, get_best_audio_format,
get_best_video_format) get_best_video_format)
from .mediaservers import PlexMediaServer
from .fields import CommaSepChoiceField from .fields import CommaSepChoiceField
from .choices import (Val, CapChoices, Fallback, FileExtension, from .choices import (Val, CapChoices, Fallback, FileExtension,
FilterSeconds, IndexSchedule, MediaServerType, FilterSeconds, IndexSchedule, MediaServerType,
@@ -1553,11 +1552,10 @@ class MediaServer(models.Model):
''' '''
ICONS = { ICONS = {
Val(MediaServerType.JELLYFIN): '<i class="fas fa-server"></i>',
Val(MediaServerType.PLEX): '<i class="fas fa-server"></i>', Val(MediaServerType.PLEX): '<i class="fas fa-server"></i>',
} }
HANDLERS = { HANDLERS = MediaServerType.handlers_dict()
Val(MediaServerType.PLEX): PlexMediaServer,
}
server_type = models.CharField( server_type = models.CharField(
_('server type'), _('server type'),
@@ -1580,17 +1578,17 @@ class MediaServer(models.Model):
) )
use_https = models.BooleanField( use_https = models.BooleanField(
_('use https'), _('use https'),
default=True, default=False,
help_text=_('Connect to the media server over HTTPS') help_text=_('Connect to the media server over HTTPS')
) )
verify_https = models.BooleanField( verify_https = models.BooleanField(
_('verify https'), _('verify https'),
default=False, default=True,
help_text=_('If connecting over HTTPS, verify the SSL certificate is valid') help_text=_('If connecting over HTTPS, verify the SSL certificate is valid')
) )
options = models.TextField( options = models.TextField(
_('options'), _('options'),
blank=True, blank=False, # valid JSON only
null=True, null=True,
help_text=_('JSON encoded options for the media server') help_text=_('JSON encoded options for the media server')
) )
@@ -14,7 +14,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<form method="post" action="{% url 'sync:add-mediaserver' server_type='plex' %}" class="col s12 simpleform"> <form method="post" action="{% url 'sync:add-mediaserver' server_type=server_type_long %}" class="col s12 simpleform">
{% csrf_token %} {% csrf_token %}
{% include 'simpleform.html' with form=form %} {% include 'simpleform.html' with form=form %}
<div class="row no-margin-bottom padding-top"> <div class="row no-margin-bottom padding-top">
@@ -10,16 +10,18 @@
Media servers are services like Plex which you may be running on your network. If Media servers are services like Plex which you may be running on your network. If
you add your media server TubeSync will notify your media server to rescan or you add your media server TubeSync will notify your media server to rescan or
refresh its libraries every time media is successfully downloaded. Currently, refresh its libraries every time media is successfully downloaded. Currently,
TubeSync only supports Plex. TubeSync only supports Jellyfin and Plex.
</p> </p>
</div> </div>
</div> </div>
{% include 'infobox.html' with message=message %} {% include 'infobox.html' with message=message %}
{% for mst in media_server_types %}
<div class="row"> <div class="row">
<div class="col s12 margin-bottom"> <div class="col s12 margin-bottom">
<a href="{% url 'sync:add-mediaserver' server_type='plex' %}" class="btn">Add a Plex media server <i class="fas fa-server"></i></a> <a href="{% url 'sync:add-mediaserver' server_type=mst.long_type %}" class="btn">Add a {{ mst.label }} media server <i class="fas fa-server"></i></a>
</div> </div>
</div> </div>
{% endfor %}
<div class="row no-margin-bottom"> <div class="row no-margin-bottom">
<div class="col s12"> <div class="col s12">
<div class="collection"> <div class="collection">
+10 -11
View File
@@ -25,7 +25,7 @@ from common.utils import append_uri_params
from background_task.models import Task, CompletedTask from background_task.models import Task, CompletedTask
from .models import Source, Media, MediaServer from .models import Source, Media, MediaServer
from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm, from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm,
SkipMediaForm, EnableMediaForm, ResetTasksForm, PlexMediaServerForm, SkipMediaForm, EnableMediaForm, ResetTasksForm,
ConfirmDeleteMediaServerForm) ConfirmDeleteMediaServerForm)
from .utils import validate_url, delete_file from .utils import validate_url, delete_file
from .tasks import (map_task_to_instance, get_error_message, from .tasks import (map_task_to_instance, get_error_message,
@@ -879,6 +879,7 @@ class MediaServersView(ListView):
template_name = 'sync/mediaservers.html' template_name = 'sync/mediaservers.html'
context_object_name = 'mediaservers' context_object_name = 'mediaservers'
types_object = MediaServerType
messages = { messages = {
'deleted': _('Your selected media server has been deleted.'), 'deleted': _('Your selected media server has been deleted.'),
} }
@@ -893,11 +894,12 @@ class MediaServersView(ListView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
return MediaServer.objects.all().order_by('host') return MediaServer.objects.all().order_by('host', 'port')
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs) data = super().get_context_data(*args, **kwargs)
data['message'] = self.message data['message'] = self.message
data['media_server_types'] = self.types_object.members_list()
return data return data
@@ -908,13 +910,9 @@ class AddMediaServerView(FormView):
''' '''
template_name = 'sync/mediaserver-add.html' template_name = 'sync/mediaserver-add.html'
server_types = { server_types = MediaServerType.long_types()
'plex': Val(MediaServerType.PLEX),
}
server_type_names = dict(MediaServerType.choices) server_type_names = dict(MediaServerType.choices)
forms = { forms = MediaServerType.forms_dict()
Val(MediaServerType.PLEX): PlexMediaServerForm,
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.server_type = None self.server_type = None
@@ -928,6 +926,8 @@ class AddMediaServerView(FormView):
if not self.server_type: if not self.server_type:
raise Http404 raise Http404
self.form_class = self.forms.get(self.server_type) self.form_class = self.forms.get(self.server_type)
if not self.form_class:
raise Http404
self.model_class = MediaServer(server_type=self.server_type) self.model_class = MediaServer(server_type=self.server_type)
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@@ -969,6 +969,7 @@ class AddMediaServerView(FormView):
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs) data = super().get_context_data(*args, **kwargs)
data['server_type'] = self.server_type data['server_type'] = self.server_type
data['server_type_long'] = self.server_types.get(self.server_type)
data['server_type_name'] = self.server_type_names.get(self.server_type) data['server_type_name'] = self.server_type_names.get(self.server_type)
data['server_help'] = self.model_class.get_help_html() data['server_help'] = self.model_class.get_help_html()
return data return data
@@ -1029,9 +1030,7 @@ class UpdateMediaServerView(FormView, SingleObjectMixin):
template_name = 'sync/mediaserver-update.html' template_name = 'sync/mediaserver-update.html'
model = MediaServer model = MediaServer
forms = { forms = MediaServerType.forms_dict()
Val(MediaServerType.PLEX): PlexMediaServerForm,
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.object = None self.object = None