Skip to content

Commit

Permalink
Merge pull request #402 from wizarrrr/develop
Browse files Browse the repository at this point in the history
  • Loading branch information
JamsRepos authored May 3, 2024
2 parents 01ff770 + f8c13ec commit 6fd59ec
Show file tree
Hide file tree
Showing 52 changed files with 722 additions and 744 deletions.
147 changes: 147 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

14 changes: 3 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ If you wish to stay up-to-date with our progress, make sure to join our [Discord

## What is Wizarr?

Wizarr is an automated user invitation system for Plex/Jellyfin/Emby. You can create a unique link, share it with a user, and they will be invited to your Media Server after they complete the simple signup process!
Wizarr is an automated user invitation system compatible with Plex, Jellyfin and Emby. You can create a unique link, share it with a user, and they will be invited to your Media Server after they complete the simple signup process!

## Major Features Include

Expand All @@ -46,20 +46,12 @@ Wizarr is an automated user invitation system for Plex/Jellyfin/Emby. You can cr
- Session Management for Admin Users
- Scheduled Tasks to keep Wizarr updated with your Media Server
- Live logs directly from the Wizarr Web UI

## What is the difference between V3 and V4?

V3 is the current stable version of Wizarr; It is a fully functional system that allows you to invite users to your Plex/Jellyfin/Emby media server. V4 is the next iteration which will expand on features considerably. For now, however, a few changes have already been added:

- Administrator password change
- Linking and unlinking your Discord server to/from the onboarding process
- Updated logo and branding

## Major features to come in V4 will include

- Custom onboarding builder
- Customizable onboarding and branding
- Added API Endpoints (already partially available)
- Plex/Jellyfin/Emby granular user permissions
- Plex/Jellyfin/Emby granular user permissions/profiles
- Discord invite request integration
- Multi-Server Support
- SMTP Support for notifications and user invites
Expand Down
15 changes: 1 addition & 14 deletions apps/wizarr-backend/wizarr_backend/api/routes/users_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from flask_restx import Namespace, Resource
from json import loads, dumps

from helpers.universal import global_sync_users_to_media_server, global_delete_user_from_media_server, global_get_user_profile_picture
from helpers.universal import global_sync_users_to_media_server, global_delete_user_from_media_server
from app.models.database.users import Users

from app.extensions import cache
Expand Down Expand Up @@ -40,19 +40,6 @@ def delete(self, user_id):
return global_delete_user_from_media_server(user_id), 200


@api.route("/<string:user_id>/profile-picture")
class UsersProfilePictureAPI(Resource):

method_decorators = [jwt_required()]

@cache.cached(timeout=3600)
@api.doc(description="Get a user profile picture")
@api.response(500, "Internal server error")
def get(self, user_id):
picture = global_get_user_profile_picture(user_id)
return send_file(picture, mimetype="image/jpeg")


@api.route("/scan")
class UsersScanAPI(Resource):

Expand Down
2 changes: 2 additions & 0 deletions apps/wizarr-backend/wizarr_backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .security import *
from .utils.clear_logs import clear_logs
from .migrator import run_migrations
from .utils.software_lifecycle import get_current_version

from sentry_sdk import init as sentry_init

Expand All @@ -26,6 +27,7 @@
enable_tracing=True,
traces_sample_rate=1.0,
environment=app.debug and "development" or "production",
release=str(get_current_version()),
)

# Base route for testing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#
# CREATED ON VERSION: V4.0.0
# MIGRATION: 2024-04-30_16-44-17
# CREATED: Tue Apr 30 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, its a boolean field with a default value of True
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 "hide_user" not in column_names:
db.execute_sql("ALTER TABLE invitations ADD COLUMN hide_user BOOLEAN DEFAULT 1")
elif "live_tv" in column_names:
db.execute_sql("ALTER TABLE invitations ALTER COLUMN live_tv BOOLEAN;")
else:
print("Column hide_user already exists")

print("Migration 2024-04-30_16-44-17 complete")
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ class Invitations(BaseModel):
plex_home = BooleanField(null=True, default=None)
sessions = CharField(null=True, default=None)
live_tv = BooleanField(null=True, default=None)
hide_user = BooleanField(null=True, default=True)
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ class InvitationsModel(Model):
specific_libraries = SpecificLibrariesType(required=False, default=[])
plex_allow_sync = BooleanType(required=False, default=False)
sessions = IntType(required=False, default=None)
live_tv = BooleanType(required=False, default=False)
live_tv = BooleanType(required=False, default=True)
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)
hide_user = BooleanType(required=False, default=True)


# ANCHOR - Validate Code
Expand Down
77 changes: 11 additions & 66 deletions apps/wizarr-backend/wizarr_backend/helpers/emby.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
# - Emby Delete User
# - Emby Sync Users


# ANCHOR - Emby Get Request
def get_emby(api_path: str, as_json: Optional[bool] = True, server_api_key: Optional[str] = None, server_url: Optional[str] = None):
"""Get data from Emby.
Expand Down Expand Up @@ -253,20 +252,18 @@ def invite_emby_user(username: str, password: str, code: str, server_api_key: Op
if invitation.specific_libraries is not None and len(invitation.specific_libraries) > 0:
sections = invitation.specific_libraries.split(",")

print(sections)

# Create user object
new_user = { "Name": str(username) }

# Create user in Emby
user_response = post_emby(api_path="/Users/New", json=new_user, server_api_key=server_api_key, server_url=server_url)

# Set user password
post_emby(api_path=f"/Users/{user_response['Id']}/Password", json={"NewPw": str(password)}, server_api_key=server_api_key, server_url=server_url)

# Create policy object
new_policy = {
"EnableAllFolders": True,
"SimultaneousStreamLimit": 0,
"EnableLiveTvAccess": False,
"EnableLiveTvManagement": False,
"AuthenticationProviderId": "Emby.Server.Implementations.Library.DefaultAuthenticationProvider",
}

Expand All @@ -278,14 +275,14 @@ def invite_emby_user(username: str, password: str, code: str, server_api_key: Op
# Set stream limit options
if invitation.sessions is not None and int(invitation.sessions) > 0:
new_policy["SimultaneousStreamLimit"] = int(invitation.sessions)
else:
new_policy["SimultaneousStreamLimit"] = 0

# Set live tv access
if invitation.live_tv is not None and invitation.live_tv == True:
new_policy["EnableLiveTvAccess"] = True
else:
new_policy["EnableLiveTvAccess"] = False

# Set the hidden user status
if invitation.hide_user is not None and invitation.hide_user == False:
new_policy["IsHiddenRemotely"] = False

# Get users default policy
old_policy = user_response["Policy"]
Expand All @@ -299,6 +296,9 @@ def invite_emby_user(username: str, password: str, code: str, server_api_key: Op
# Update user policy
post_emby(api_path=api_path, json=new_policy, server_api_key=server_api_key, server_url=server_url)

# Set user password, this is done after the policy is set due to LDAP policies
post_emby(api_path=f"/Users/{user_response['Id']}/Password", json={"NewPw": str(password)}, server_api_key=server_api_key, server_url=server_url)

# Return response
return user_response

Expand Down Expand Up @@ -405,59 +405,4 @@ def sync_emby_users(server_api_key: Optional[str] = None, server_url: Optional[s
for database_user in database_users:
if str(database_user.token) not in [str(emby_user["Id"]) for emby_user in emby_users]:
database_user.delete_instance()
info(f"User {database_user.username} successfully deleted from database.")



# ANCHOR - Emby Get Profile Picture
def get_emby_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 Emby.
:param user_id: ID of the user to get profile picture for
:type user_id: str
:param username: Username for backup profile picture using ui-avatars.com
:type username: str
:param max_height: Maximum height of profile picture
:type max_height: Optional[int] - Default: 150
:param max_width: Maximum width of profile picture
:type max_width: Optional[int] - Default: 150
:param quality: Quality of profile picture
:type quality: Optional[int] - Default: 30
:param server_api_key: Emby API key
:type server_api_key: Optional[str] - If not provided, will get from database.
:param server_url: Emby URL
:type server_url: Optional[str] - If not provided, will get from database.
:return: Emby API response
"""

# Response object
response = None

try:
# Get profile picture from Emby
response = get_emby(api_path=f"/Users/{user_id}/Images/Primary?maxHeight={max_height}&maxWidth={max_width}&quality={quality}", as_json=False, server_api_key=server_api_key, server_url=server_url)
except RequestException:
# Backup profile picture using ui-avatars.com if Emby fails
user = get_user_by_token(user_id, verify=False)
username = f"{user.username}&length=1" if user else "ERROR&length=60&font-size=0.28"
response = get(url=f"https://ui-avatars.com/api/?uppercase=true&name={username}", timeout=30)

# Raise exception if either Emby or ui-avatars.com fails
if response.status_code != 200:
raise RequestException("Failed to get profile picture.")

# Extract image from response
image = response.content

# Convert image bytes to read image
image = BytesIO(image)

# Return profile picture
return image
info(f"User {database_user.username} successfully deleted from database.")
68 changes: 8 additions & 60 deletions apps/wizarr-backend/wizarr_backend/helpers/jellyfin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
# - Jellyfin Delete User
# - Jellyfin Sync Users


# ANCHOR - Jellyfin Get Request
def get_jellyfin(api_path: str, as_json: Optional[bool] = True, server_api_key: Optional[str] = None, server_url: Optional[str] = None):
"""Get data from Jellyfin.
Expand Down Expand Up @@ -262,6 +261,9 @@ def invite_jellyfin_user(username: str, password: str, code: str, server_api_key
# Create policy object
new_policy = {
"EnableAllFolders": True,
"MaxActiveSessions": 0,
"EnableLiveTvAccess": False,
"EnableLiveTvManagement": False,
"AuthenticationProviderId": "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider",
}

Expand All @@ -273,14 +275,14 @@ def invite_jellyfin_user(username: str, password: str, code: str, server_api_key
# 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

# Set live tv access
if invitation.live_tv is not None and invitation.live_tv == True:
new_policy["EnableLiveTvAccess"] = True
else:
new_policy["EnableLiveTvAccess"] = False

# Set the hidden user status
if invitation.hide_user is not None and invitation.hide_user == False:
new_policy["IsHidden"] = False

# Get users default policy
old_policy = user_response["Policy"]
Expand Down Expand Up @@ -397,58 +399,4 @@ def sync_jellyfin_users(server_api_key: Optional[str] = None, server_url: Option
for database_user in database_users:
if str(database_user.token) not in [str(jellyfin_user["Id"]) for jellyfin_user in jellyfin_users]:
database_user.delete_instance()
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.
:param user_id: ID of the user to get profile picture for
:type user_id: str
:param username: Username for backup profile picture using ui-avatars.com
:type username: str
:param max_height: Maximum height of profile picture
:type max_height: Optional[int] - Default: 150
:param max_width: Maximum width of profile picture
:type max_width: Optional[int] - Default: 150
:param quality: Quality of profile picture
:type quality: Optional[int] - Default: 30
:param server_api_key: Jellyfin API key
:type server_api_key: Optional[str] - If not provided, will get from database.
:param server_url: Jellyfin URL
:type server_url: Optional[str] - If not provided, will get from database.
:return: Jellyfin API response
"""

# Response object
response = None

try:
# Get profile picture from Jellyfin
response = get_jellyfin(api_path=f"/Users/{user_id}/Images/Primary?maxHeight={max_height}&maxWidth={max_width}&quality={quality}", as_json=False, server_api_key=server_api_key, server_url=server_url)
except RequestException:
# Backup profile picture using ui-avatars.com if Jellyfin fails
user = get_user_by_token(user_id, verify=False)
username = f"{user.username}&length=1" if user else "ERROR&length=60&font-size=0.28"
response = get(url=f"https://ui-avatars.com/api/?uppercase=true&name={username}", timeout=30)

# Raise exception if either Jellyfin or ui-avatars.com fails
if response.status_code != 200:
raise RequestException("Failed to get profile picture.")

# Extract image from response
image = response.content

# Convert image bytes to read image
image = BytesIO(image)

# Return profile picture
return image
info(f"User {database_user.username} successfully deleted from database.")
Loading

0 comments on commit 6fd59ec

Please sign in to comment.