Skip to content

Commit

Permalink
feat: 🚑 added ability to specify concurrent sessions on jellyfin (#378)
Browse files Browse the repository at this point in the history
* fix(jellyfin): 🚑 non-default active user login session limit

The policy being used to create a Jellyfin user was limited to 2 login session per commit 5b67ee8.

The default setting when creating a user via the UI is 0 (unlimited).

This resolves issue [#377](#377).

* fix: 🚑 allow selectable max user sessions

* fix: 🐝 dropdown formatting issues

* fix: 🚑 ✨ Fixed custom user limit

* feat: 🎊 ✨ Added ability to set user sessions limit on Jellyfin

* revert: 🔙 🗑️ Removed old deprecated testing

* fix: 🩹 ⚰️ Removed deprecated code which is causing errors

* feat: 🚀 database migration script

* docs: 📚 add code comments

* fix: 🐝 migration script

* fix: 🐝 🚑 Fixed labels not applying to select fields

* fix: 🐝 🚑 Fixed incorrect value from field

* fix: 🩹 add sessions column to base modal

* chore: 🧼 📝 Increase dropdown to a max of 10

* fix(db-migration): 🐝 change sessions default to null

* feat: 🎊 ✨ Added ability to view server specific options on invite

* refactor: 🔧 🎨 Changed the invite view to be similar to creation

---------

Co-authored-by: Jam <1347620+JamsRepos@users.noreply.github.com>
  • Loading branch information
MrDynamo and JamsRepos committed Apr 21, 2024
1 parent 83c381a commit 062bc6b
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#
# CREATED ON VERSION: V4.0.0b7
# MIGRATION: 2024-04-19_18-46-37
# CREATED: Fri Apr 19 2024
#

from peewee import *
from playhouse.migrate import *

from app import db

# Do not change the name of this file,
# migrations are run in order of their filenames date and time


def run():
# Use migrator to perform actions on the database
migrator = SqliteMigrator(db)

# Add new Column to users table called tutorial, its a boolean field with a default value of False
with db.transaction():
# Check if the column exists
cursor = db.cursor()
cursor.execute("PRAGMA table_info(invitations);")
columns = cursor.fetchall()
column_names = [column[1] for column in columns]

if "sessions" not in column_names:
db.execute_sql("ALTER TABLE invitations ADD COLUMN sessions INTEGER")
else:
print("Column sessions already exists")

print("Migration 2024-04-19_18-46-37 complete")
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ class Invitations(BaseModel):
specific_libraries = CharField(default=None, null=True)
plex_allow_sync = BooleanField(null=True, default=None)
plex_home = BooleanField(null=True, default=None)
sessions = CharField(null=True, default=None)
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class InvitationsModel(Model):
duration = IntType(required=False, default=None)
specific_libraries = SpecificLibrariesType(required=False, default=[])
plex_allow_sync = BooleanType(required=False, default=False)
sessions = IntType(required=False, default=None)
plex_home = BooleanType(required=False, default=False)
used_at = DateTimeType(required=False, default=None, convert_tz=True)
created = DateTimeType(required=False, default=datetime.utcnow(), convert_tz=True)
Expand Down
10 changes: 8 additions & 2 deletions apps/wizarr-backend/wizarr_backend/helpers/jellyfin.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,19 @@ def invite_jellyfin_user(username: str, password: str, code: str, server_api_key
user_response = post_jellyfin(api_path="/Users/New", json=new_user, server_api_key=server_api_key, server_url=server_url)

# Create policy object
new_policy = { "EnableAllFolders": True, "MaxActiveSessions": 2 }
new_policy = {"EnableAllFolders": True}

# Set library options
if sections:
new_policy["EnableAllFolders"] = False
new_policy["EnabledFolders"] = sections

# Set session limit options
if invitation.sessions is not None and int(invitation.sessions) > 0:
new_policy["MaxActiveSessions"] = int(invitation.sessions)
else:
new_policy["MaxActiveSessions"] = 0

old_policy = user_response["Policy"]

# Merge policy with user policy don't overwrite
Expand Down Expand Up @@ -376,7 +383,6 @@ def sync_jellyfin_users(server_api_key: Optional[str] = None, server_url: Option
info(f"User {database_user.username} successfully deleted from database.")



# ANCHOR - Jellyfin Get Profile Picture
def get_jellyfin_profile_picture(user_id: str, max_height: Optional[int] = 150, max_width: Optional[int] = 150, quality: Optional[int] = 30, server_api_key: Optional[str] = None, server_url: Optional[str] = None):
"""Get profile picture from Jellyfin.
Expand Down
11 changes: 10 additions & 1 deletion apps/wizarr-frontend/src/formkit.theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const theme: Record<string, Record<string, string>> = {

"family:button": {
input: `
bg-primary hover:bg-primary_hover
bg-primary hover:bg-primary_hover
data-[theme=primary]:bg-primary data-[theme=primary]:hover:bg-primary_hover
data-[theme=secondary]:bg-secondary data-[theme=secondary]:hover:bg-secondary_hover
data-[theme=danger]:bg-red-600 data-[theme=danger]:hover:bg-red-500
Expand Down Expand Up @@ -242,6 +242,15 @@ const theme: Record<string, Record<string, string>> = {
tagsWrapper: "w-full",
tags: "flex flex-row flex-wrap w-full py-1.5 px-2",
},

number: {
input: "peer-[.formkit-prefix-icon]:pl-9 peer-[.formkit-suffix-icon]:pr-9 mb-1 w-full bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white sm:text-sm border border-gray-300 dark:border-gray-600 rounded block w-full dark:placeholder-gray-400 focus:ring-primary focus:border-primary",
label: "block mb-2 text-sm font-medium text-gray-900 dark:text-white",
inner: "w-full relative",
outer: "mb-4 formkit-disabled:opacity-50",
prefixIcon: "peer absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none bg-gray-50 dark:bg-gray-700 rounded-l border-l border-t border-b border-gray-300 dark:border-gray-600",
suffixIcon: "peer absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none bg-gray-50 dark:bg-gray-700 rounded-r border-r border-t border-b border-gray-300 dark:border-gray-600",
},
};

export default theme;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@

<Collapse :when="advancedOptions" class="space-y-4">
<!-- Select Options -->
<FormKit type="checkbox" name="options" :options="checkboxOptions" />
<FormKit type="checkbox" name="checkboxes" :options="checkboxOptions" />

<!-- Loop through selects and make a FormKit select for each -->
<template v-for="(data, label) in selectsOptions[0]" :key="label">
<FormKit type="select" :label="data.label" :name="label" :options="data.options" />
</template>

<!-- Select Duration -->
<FormKit type="select" label="User Account Duration" name="duration" :options="durationOptions" />
Expand Down Expand Up @@ -102,10 +107,11 @@ export default defineComponent({
inviteCode: "",
expiration: 1440 as number | null | "custom",
customExpiration: "" as string,
options: [] as string[],
checkboxes: [] as string[],
duration: "unlimited" as number | "unlimited" | "custom",
customDuration: "" as string,
libraries: [] as string[],
sessions: 0 as number,
},
disabled: false,
expirationOptions: [
Expand Down Expand Up @@ -168,7 +174,7 @@ export default defineComponent({
value: "custom",
},
],
options: {
checkboxes: {
jellyfin: {
unlimited: {
label: "Unlimited Invitation Usages",
Expand Down Expand Up @@ -196,6 +202,26 @@ export default defineComponent({
},
},
} as Record<string, Record<string, { label: string; value: string }>>,
selects: {
jellyfin: {
sessions: {
label: "Maximum Number of Simultaneous User Logins",
options: {
0: "No Limit",
1: "1 Session",
2: "2 Sessions",
3: "3 Sessions",
4: "4 Sessions",
5: "5 Sessions",
6: "6 Sessions",
7: "7 Sessions",
8: "8 Sessions",
9: "9 Sessions",
10: "10 Sessions",
},
},
},
} as Record<string, Record<string, { label: string; options: Record<number, string> }>>,
advancedOptions: false,
clipboardToast: null as ToastID | null,
};
Expand All @@ -209,9 +235,10 @@ export default defineComponent({
// Parse the data ready for the API
const code = invitationData.inviteCode;
const expires = invitationData.expiration == "custom" ? this.$filter("toMinutes", invitationData.customExpiration) : invitationData.expiration;
const unlimited = invitationData.options.includes("unlimited");
const plex_home = invitationData.options.includes("plex_home");
const plex_allow_sync = invitationData.options.includes("plex_allow_sync");
const unlimited = invitationData.checkboxes.includes("unlimited");
const plex_home = invitationData.checkboxes.includes("plex_home");
const plex_allow_sync = invitationData.checkboxes.includes("plex_allow_sync");
const sessions = invitationData.sessions;
const duration = invitationData.duration == "custom" ? this.$filter("toMinutes", invitationData.customDuration) : invitationData.duration == "unlimited" ? null : invitationData.duration;
const libraries = invitationData.libraries;
Expand All @@ -221,6 +248,7 @@ export default defineComponent({
unlimited: unlimited,
plex_home: plex_home,
plex_allow_sync: plex_allow_sync,
sessions: sessions,
duration: duration,
specific_libraries: JSON.stringify(libraries),
};
Expand Down Expand Up @@ -299,8 +327,13 @@ export default defineComponent({
return this.$filter("toMinutes", this.invitationData.customDuration, true);
},
checkboxOptions() {
return Object.keys(this.options[this.settings.server_type]).map((key) => {
return this.options[this.settings.server_type][key];
return Object.keys(this.checkboxes[this.settings.server_type]).map((key) => {
return this.checkboxes[this.settings.server_type][key];
});
},
selectsOptions() {
return Object.keys(this.selects[this.settings.server_type]).map((key) => {
return this.selects[this.settings.server_type];
});
},
librariesOptions(): { label: string; value: string }[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,12 @@
{{ __('Invitation Settings') }}
</h2>
<div class="flex flex-col space-y-2">
<template v-for="item in quickList">
<span
:class="
item.value
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-green-400 border-green-400'
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-red-400 border-red-400'
"
class="text-xs font-medium px-2.5 py-1 rounded border"
>
{{ item.text }}
</span>
<template v-for="(data, label) in checkboxOptions[0]" :key="label">
<FormKit type="checkbox" :label="data.label" :value="data.value" disabled />
</template>

<template v-for="(data, label) in selectsOptions[0]" :key="label">
<FormKit type="select" :label="data.label" :name="label" :options="data.options" :value="data.value" disabled />
</template>
</div>
</div>
Expand All @@ -69,6 +64,8 @@

<script lang="ts">
import { defineComponent } from 'vue';
import { mapState, mapActions } from "pinia";
import { useServerStore } from "@/stores/server";
import { useClipboard } from '@vueuse/core';
import type { Invitation } from '@/types/api/invitations';
Expand Down Expand Up @@ -99,20 +96,55 @@ export default defineComponent({
},
],
invitationExpired: true,
quickList: [
{
text: 'Unlimited',
value: this.invitation.unlimited,
checkboxes: {
jellyfin: {
unlimited: {
label: "Unlimited Invitation Usages",
value: this.invitation.unlimited,
},
},
{
text: 'Allow Downloads',
value: this.invitation.plex_allow_sync,
emby: {
unlimited: {
label: "Unlimited Invitation Usages",
value: this.invitation.unlimited,
},
},
{
text: 'Plex Home',
value: this.invitation.plex_home,
plex: {
unlimited: {
label: "Unlimited Invitation Usages",
value: this.invitation.plex_home,
},
plex_home: {
label: "Allow Plex Home Access",
value: this.invitation.plex_home,
},
plex_allow_sync: {
label: "Allow Plex Downloads",
value: this.invitation.plex_allow_sync,
},
},
],
} as Record<string, Record<string, { label: string; value: boolean }>>,
selects: {
jellyfin: {
sessions: {
label: "Maximum Number of Simultaneous User Logins",
value: this.invitation.sessions,
options: {
0: "No Limit",
1: "1 Session",
2: "2 Sessions",
3: "3 Sessions",
4: "4 Sessions",
5: "5 Sessions",
6: "6 Sessions",
7: "7 Sessions",
8: "8 Sessions",
9: "9 Sessions",
10: "10 Sessions",
},
},
},
} as Record<string, Record<string, { label: string; value: number, options: Record<number, string> }>>,
clipboard: useClipboard({
legacy: true,
}),
Expand All @@ -136,6 +168,17 @@ export default defineComponent({
invitationExpiredDateReadable(): string {
return new Date(this.invitation.expires).toLocaleString();
},
checkboxOptions() {
return Object.keys(this.checkboxes[this.settings.server_type]).map((key) => {
return this.checkboxes[this.settings.server_type];
});
},
selectsOptions() {
return Object.keys(this.selects[this.settings.server_type]).map((key) => {
return this.selects[this.settings.server_type];
});
},
...mapState(useServerStore, ["settings"]),
},
methods: {
invitationCodeToggle() {
Expand Down
1 change: 1 addition & 0 deletions apps/wizarr-frontend/src/types/api/invitations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Invitation {
id: number;
plex_allow_sync: boolean;
plex_home: boolean;
sessions: number;
specific_libraries: string;
unlimited: boolean;
used: boolean;
Expand Down

0 comments on commit 062bc6b

Please sign in to comment.