Skip to content

Commit

Permalink
Connect the UI for Jellyfin
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tcely authored Feb 22, 2025
1 parent e25a9cd commit 9761017
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 34 deletions.
47 changes: 47 additions & 0 deletions tubesync/sync/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
32 changes: 32 additions & 0 deletions tubesync/sync/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
24 changes: 11 additions & 13 deletions tubesync/sync/mediaservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,32 +180,31 @@ def make_request(self, uri='/', params={}):
'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):
if not self.object.host:
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'})
Expand Down Expand Up @@ -234,19 +233,18 @@ def validate(self):
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
Expand Down
33 changes: 33 additions & 0 deletions tubesync/sync/migrations/0029_alter_mediaserver_fields.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
12 changes: 5 additions & 7 deletions tubesync/sync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
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,
Expand Down Expand Up @@ -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'),
Expand All @@ -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')
)
Expand Down
2 changes: 1 addition & 1 deletion tubesync/sync/templates/sync/mediaserver-add.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ <h1>Add a {{ server_type_name }} media server</h1>
</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">
Expand Down
6 changes: 4 additions & 2 deletions tubesync/sync/templates/sync/mediaservers.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ <h1 class="truncate">Media servers</h1>
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">
Expand Down
21 changes: 10 additions & 11 deletions tubesync/sync/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
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,
Expand Down Expand Up @@ -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.'),
}
Expand All @@ -893,11 +894,12 @@ def dispatch(self, request, *args, **kwargs):
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


Expand All @@ -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
Expand All @@ -928,6 +926,8 @@ def dispatch(self, request, *args, **kwargs):
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)

Expand Down Expand Up @@ -969,6 +969,7 @@ def form_valid(self, form):
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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9761017

Please sign in to comment.