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
+137 -28

No files matched your search

+47
View File
@@ -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'
+32
View File
@@ -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(
+5 -7
View File
@@ -180,9 +180,8 @@ 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)
@@ -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
@@ -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)
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): '<i class="fas fa-server"></i>',
Val(MediaServerType.PLEX): '<i class="fas fa-server"></i>',
}
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')
)
@@ -14,7 +14,7 @@
</div>
</div>
<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 %}
{% include 'simpleform.html' with form=form %}
<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
you add your media server TubeSync will notify your media server to rescan or
refresh its libraries every time media is successfully downloaded. Currently,
TubeSync only supports Plex.
TubeSync only supports Jellyfin and Plex.
</p>
</div>
</div>
{% include 'infobox.html' with message=message %}
{% for mst in media_server_types %}
<div class="row">
<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>
{% endfor %}
<div class="row no-margin-bottom">
<div class="col s12">
<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 .models import Source, Media, MediaServer
from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm,
SkipMediaForm, EnableMediaForm, ResetTasksForm, PlexMediaServerForm,
SkipMediaForm, EnableMediaForm, ResetTasksForm,
ConfirmDeleteMediaServerForm)
from .utils import validate_url, delete_file
from .tasks import (map_task_to_instance, get_error_message,
@@ -879,6 +879,7 @@ class MediaServersView(ListView):
template_name = 'sync/mediaservers.html'
context_object_name = 'mediaservers'
types_object = MediaServerType
messages = {
'deleted': _('Your selected media server has been deleted.'),
}
@@ -893,11 +894,12 @@ class MediaServersView(ListView):
return super().dispatch(request, *args, **kwargs)
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):
data = super().get_context_data(*args, **kwargs)
data['message'] = self.message
data['media_server_types'] = self.types_object.members_list()
return data
@@ -908,13 +910,9 @@ class AddMediaServerView(FormView):
'''
template_name = 'sync/mediaserver-add.html'
server_types = {
'plex': Val(MediaServerType.PLEX),
}
server_types = MediaServerType.long_types()
server_type_names = dict(MediaServerType.choices)
forms = {
Val(MediaServerType.PLEX): PlexMediaServerForm,
}
forms = MediaServerType.forms_dict()
def __init__(self, *args, **kwargs):
self.server_type = None
@@ -928,6 +926,8 @@ class AddMediaServerView(FormView):
if not self.server_type:
raise Http404
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)
return super().dispatch(request, *args, **kwargs)
@@ -969,6 +969,7 @@ class AddMediaServerView(FormView):
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs)
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_help'] = self.model_class.get_help_html()
return data
@@ -1029,9 +1030,7 @@ class UpdateMediaServerView(FormView, SingleObjectMixin):
template_name = 'sync/mediaserver-update.html'
model = MediaServer
forms = {
Val(MediaServerType.PLEX): PlexMediaServerForm,
}
forms = MediaServerType.forms_dict()
def __init__(self, *args, **kwargs):
self.object = None