• Login
  • Register
  • Login Register
    Login
    Username/Email:
    Password:
    Or login with a social network below
  • Forum
  • Website
  • GitHub
  • Status
  • Translation
  • Features
  • Team
  • Rules
  • Help
  • Feeds
User Links
  • Login
  • Register
  • Login Register
    Login
    Username/Email:
    Password:
    Or login with a social network below

    Useful Links Forum Website GitHub Status Translation Features Team Rules Help Feeds
    Jellyfin Forum Support Guides, Walkthroughs & Tutorials Playlist sharing with write permission

     
    • 0 Vote(s) - 0 Average

    Playlist sharing with write permission

    script to grant permission to different user to edit the same playlist
    I-G-1-1
    Offline

    Junior Member

    Posts: 32
    Threads: 5
    Joined: 2023 Jun
    Reputation: 0
    #1
    Yesterday, 01:35 AM (This post was last modified: Yesterday, 01:52 AM by I-G-1-1. Edited 2 times in total.)
    I (and some AI) wrote this code to make possible to grant write permission to a shared playlist to a user that doesn't own the playlist.

    At some point It was really simple to achieve write permission simple adding this:

      <Shares>
        <Share>
          <UserId>xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx</UserId>
          <CanEdit>true</CanEdit>
        </Share>
      </Shares>

    to the /var/lib/jellyfin/data/playlists/[PLAYLIST_NAME]/playlist.xml of any playlist.

    At the moment in Jellyfin 10.10.7 this manual change doesn't grant any permission and as soon as I add a song to the playlist the manual change is deleted from the playlist.xml file.

    So It seems that the only way to grant write permission to a shared playlist is using the API (https://api.jellyfin.org/#tag/Playlists/...aylistUser). So I (and the AI) had to write this python script.

    HOW TO INSTALL:

    sudo apt-get install python3.11-venv
    cd
    mkdir JFPllstAccssCntrll
    cd JFPllstAccssCntrll
    python3 -m venv .
    source ~/JFPllstAccssCntrll/bin/activate
    nano requirements.txt
    '
    requests>=2.0
    '
    pip3 install -r requirements.txt


    nano JFPllstAccssCntrll.py and paste the script:

    Code:
    #!/usr/bin/env python3
    """
    Script to manage Jellyfin playlist write permissions.

      • --list            : group by owner and print ASCII subtables
      • [user] [playlist] : grant or revoke edit permission (use --revoke)
      • --delay N        : seconds to wait after refresh before listing (default 5)
    """

    import os
    import sqlite3
    import argparse
    import getpass
    import requests
    import xml.etree.ElementTree as ET
    import time

    # --- 1) CONFIGURATION --------------------------------------------------

    JELLYFIN_BASE = "http://localhost:8096"
    PLAYLISTS_DIR = "/var/lib/jellyfin/data/playlists"
    LIBRARY_DB    = "/var/lib/jellyfin/data/library.db"
    USERS_DB      = "/var/lib/jellyfin/data/jellyfin.db"

    CLIENT_HDR = (
        'Mediabrowser Client="script", '
        'Device="script", DeviceId="script", Version="1.0.0"'
    )

    # --- 2) AUTH UTILITIES -------------------------------------------------

    def normalize_id(raw_id):
        return raw_id.replace("-", "").lower()

    def authenticate(username):
        """
        Prompt for a password and authenticate to Jellyfin by username.
        Returns an access token.
        """
        pw = getpass.getpass(f"Password for '{username}': ")
        url = f"{JELLYFIN_BASE}/Users/AuthenticateByName"
        headers = {
            "accept": "application/json",
            "Content-Type": "application/json",
            "x-emby-authorization": CLIENT_HDR
        }
        resp = requests.post(url, json={"Username": username, "Pw": pw}, headers=headers)
        resp.raise_for_status()
        data = resp.json()
        return data.get("Data", data).get("AccessToken")

    # --- 3) SINGLE-PLAYLIST REFRESH ----------------------------------------

    def refresh_playlist(playlist_id, token):
        """
        Force reload of the specified playlist.
        Requires an Admin access token.
        """
        url = f"{JELLYFIN_BASE}/Items/{playlist_id}/Refresh"
        headers = {
            "accept": "application/json",
            "Content-Type": "application/json",
            "X-MediaBrowser-Token": token
        }
        resp = requests.post(url, headers=headers)
        try:
            resp.raise_for_status()
            print(f"✔ Playlist {playlist_id} successfully refreshed (Admin).")
        except Exception as e:
            print(f"✗ Error refreshing playlist {playlist_id}: {e}")

    # --- 4) DATABASE AND FILESYSTEM LOADERS ---------------------------------

    def load_users():
        conn = sqlite3.connect(USERS_DB)
        cur  = conn.cursor()
        cur.execute("SELECT Username, Id FROM Users")
        mapping = { normalize_id(uid): name for name, uid in cur.fetchall() }
        conn.close()
        return mapping

    def load_playlists():
        conn = sqlite3.connect(LIBRARY_DB)
        cur  = conn.cursor()
        cur.execute("""
            SELECT Name, PresentationUniqueKey
            FROM TypedBaseItems
            WHERE Type='MediaBrowser.Controller.Playlists.Playlist'
        """)
        mapping = { name: pid.lower() for name, pid in cur.fetchall() if pid }
        conn.close()
        return mapping

    def load_owners():
        owners = {}
        for d in os.listdir(PLAYLISTS_DIR):
            xml_path = os.path.join(PLAYLISTS_DIR, d, "playlist.xml")
            if not os.path.isfile(xml_path):
                continue
            tree = ET.parse(xml_path)
            owner_id = tree.findtext("OwnerUserId")
            if owner_id:
                owners[d] = normalize_id(owner_id)
        return owners

    # --- 5) LIST PERMISSIONS GROUPED BY OWNER -----------------------------

    def list_permissions():
        """
        Group playlists by owner and print ASCII subtables.
        No authentication needed since it reads from disk/DB only.
        """
        users    = load_users()
        playlists = load_playlists()
        owners    = load_owners()

        print("\n=== Current Permissions ===\n")
        by_owner = {}
        for pname in playlists:
            owner_norm = owners.get(pname)
            if not owner_norm:
                continue
            by_owner.setdefault(owner_norm, []).append(pname)

        for owner_norm, pnames in by_owner.items():
            owner_name = users.get(owner_norm, "<unknown>")
            print(f"Owner: {owner_name} ({owner_norm})")
            rows = []
            for pname in sorted(pnames):
                xml_path = os.path.join(PLAYLISTS_DIR, pname, "playlist.xml")
                editors  = []
                if os.path.isfile(xml_path):
                    root = ET.parse(xml_path).getroot()
                    for share in root.findall("Shares/Share"):
                        if share.findtext("CanEdit") == "true":
                            uid_norm = normalize_id(share.findtext("UserId") or "")
                            if uid_norm != owner_norm:
                                editors.append(users.get(uid_norm, uid_norm))
                rows.append([pname, ", ".join(editors) or "-"])

            headers    = ["Playlist", "CanEdit by"]
            col_widths = [max(len(str(c)) for c in col) for col in zip(headers, *rows)]
            sep = "+" + "+".join("-"*(w+2) for w in col_widths) + "+"

            print(sep)
            print("| " + " | ".join(h.ljust(w) for h, w in zip(headers, col_widths)) + " |")
            print(sep)
            for row in rows:
                print("| " + " | ".join(str(c).ljust(w) for c, w in zip(row, col_widths)) + " |")
            print(sep + "\n")

    # --- 6) MODIFY PERMISSION + AUTO-LIST ---------------------------------

    def modify_permission(target_user, playlist, revoke=False, delay=5):
        users    = load_users()
        playlists = load_playlists()
        owners    = load_owners()

        if playlist not in playlists:
            raise SystemExit(f"Playlist '{playlist}' not found.")
        if target_user not in users.values():
            raise SystemExit(f"User '{target_user}' not found.")

        pid  = playlists[playlist]
        tuid  = next(u for u, n in users.items() if n == target_user)
        onorm = owners.get(playlist)
        if not onorm:
            raise SystemExit("Cannot determine playlist owner.")

        # 1) Owner authentication and grant/revoke
        owner_name  = users[onorm]
        print(f"Playlist '{playlist}' ({pid}) is owned by {owner_name}.")
        token_owner = authenticate(owner_name)

        headers = {
            "accept":              "application/json",
            "Content-Type":        "application/json",
            "X-MediaBrowser-Token": token_owner
        }
        url  = f"{JELLYFIN_BASE}/Playlists/{pid}/Users/{tuid}"
        body = {"UserId": tuid, "CanEdit": not revoke}
        resp = requests.post(url, headers=headers, json=body)
        action = "revoked" if revoke else "granted"
        if resp.status_code in (200, 204):
            print(f"Edit permission {action} for '{target_user}'.")
        else:
            print(f"Error {resp.status_code}: {resp.text}")
            return

        # 2) Admin authentication and refresh single playlist
        print("Admin authentication to refresh playlist…")
        admin_user  = input("Admin username: ")
        admin_token = authenticate(admin_user)
        print(f"Triggering refresh for playlist '{playlist}' ({pid})…")
        refresh_playlist(pid, admin_token)

        # 2.b) Delay to allow DB to synchronize
        if delay > 0:
            print(f"Waiting {delay} seconds to allow DB update…")
            time.sleep(delay)

        # 3) Immediately show updated permissions
        list_permissions()

    # --- 7) ENTRYPOINT AND ARG PARSING -------------------------------------

    def main():
        parser = argparse.ArgumentParser(
            description="Manage Jellyfin playlist write permissions"
        )
        parser.add_argument("user", nargs="?", help="Target username")
        parser.add_argument("playlist", nargs="?", help="Playlist name")
        parser.add_argument("--revoke", action="store_true", help="Revoke edit permission")
        parser.add_argument("--list", action="store_true",
                            help="List permissions grouped by owner")
        parser.add_argument("--delay", type=int, default=5,
                            help="Seconds to wait after refresh (default: 5)")
        args = parser.parse_args()

        if args.list:
            list_permissions()
        elif args.user and args.playlist:
            modify_permission(args.user, args.playlist,
                              revoke=args.revoke,
                              delay=args.delay)
        else:
            parser.print_help()

    if __name__ == "__main__":
        main()

    HOW TO USE: (it needs sudo only because it needs to access to the jellyfin database so or you run it as the jellyfin user or you run it with sudo)
    sudo ~/JFPllstAccssCntrll/bin/python3 ~/JFPllstAccssCntrll/JFPllstAccssCntrll.py

    Code:
    usage: JFPllstAccssCntrll.py [-h] [--revoke] [--list] [--delay DELAY] [user] [playlist]
    Manage Jellyfin playlist write permissions
    positional arguments:
      user           Target username
      playlist       Playlist name
    options:
      -h, --help     show this help message and exit
      --revoke       Revoke edit permission
      --list         List permissions grouped by owner
      --delay DELAY  Seconds to wait after refresh (default: 5)


    sudo ~/JFPllstAccssCntrll/bin/python3 ~/JFPllstAccssCntrll/JFPllstAccssCntrll.py --list 
    give you a list off all the playlist divided by owner and with a column that say who can edit the playlist other than the owner. It will ask the username and the password of an Admin

    sudo ~/JFPllstAccssCntrll/bin/python3 ~/JFPllstAccssCntrll/JFPllstAccssCntrll.py [USER_TO_GRANT_PERMISSION] [PLAYLIST_NAME] 
    to grant permission. It will ask the password of the owner of the playlist.

    sudo ~/JFPllstAccssCntrll/bin/python3 ~/JFPllstAccssCntrll/JFPllstAccssCntrll.py [USER_TO_REVOKE_PERMISSION] [PLAYLIST_NAME] --revoke 
    to revoke permission. It will ask the password of the owner of the playlist.

    Now you have a shared playlist that can be changed from the users you granted permission.

    If you want to hide this playlist to specific users I suggest you to add a tag to the playlist (for example "playlist_user01") and then in "settings/user/[USERNAME]/parentalcontrol/block/Locking Items with tag" add playlist_user01 for each user you want to hide the playlist.
    « Next Oldest | Next Newest »

    Users browsing this thread:


    • View a Printable Version
    • Subscribe to this thread
    Forum Jump:

    Home · Team · Help · Contact
    © Designed by D&D - Powered by MyBB
    L


    Jellyfin

    The Free Software Media System

    Linear Mode
    Threaded Mode