• 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
    Frank23t
    Offline

    Junior Member

    Posts: 1
    Threads: 0
    Joined: 2025 Aug
    Reputation: 0
    Country:United States
    #2
    2025-08-22, 06:50 AM
    (2025-08-13, 01:35 AM)I-G-1-1 Wrote: 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.
    hot games
    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.
    This script helps grant or revoke playlist editing permissions in Jellyfin via API, more convenient and secure than manually editing files.
    « Next Oldest | Next Newest »

    Users browsing this thread: 1 Guest(s)


    Messages In This Thread
    Playlist sharing with write permission - by I-G-1-1 - 2025-08-13, 01:35 AM
    RE: Playlist sharing with write permission - by Frank23t - 2025-08-22, 06:50 AM

    • 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