pkkid / python-plexapi

Python bindings for the Plex API.
BSD 3-Clause "New" or "Revised" License
1.11k stars 198 forks source link

Is there any way to query Tidal via Plex, and add songs from Tidal to a Plex playlist? #1257

Closed armandhammer closed 9 months ago

armandhammer commented 9 months ago

Describe the Bug

I can't figure out if there already exists a way to query a track on Tidal via Plex, and then add that track to a playlist in my Plex music library. I've figured out how to do the query (music.provider.plex.tv) and obtain a list of results, but I'm lost on how to add this to a playlist. I have a program where it can read a list of songs, search my local library, and add those tracks to a playlist via the python plex api (I attached the code to this post). But I'd like to modify it so that if a song isn't found in my library, it searches Tidal, and adds the Tidal track to my playlist too.

Help please? I've searched the documentation and can't find any relevant mentions of Tidal nor music.provider.plex.tv

Code Snippets

from plexapi.server import PlexServer
import re

# Plex server URL and Plex Token
PLEX_URL = 'http://[MY PLEX IP]:32400'
PLEX_TOKEN = '[MY PLEX TOKEN]'

# Connect to the Plex Server
plex = PlexServer(PLEX_URL, PLEX_TOKEN)

# Correct Title of your music library
section_title = 'Music'

try:
    music_library = plex.library.section(section_title)
except Exception as e:
    print(f"Error accessing section by Title: {e}")
    exit()

# Prompt the user for the playlist name
playlist_name = input("Please enter the name for the new playlist: ")

# Define the filename containing your playlist songs
filename = 'playlist.txt'
songs = []

# Read songs from the file
try:
    with open(filename, 'r') as file:
        for line in file:
            line = line.strip()
            if ' - ' in line:
                songs.append(line)
except FileNotFoundError:
    print(f"Error: '{filename}' not found.")
    exit()

items = []

def find_track(artist_name, track_title):
    print(f"Searching for Artist: {artist_name}")
    artist_search = music_library.search(title=artist_name)
    if not artist_search:
        print(f"Artist {artist_name} not found!")
        return None

    found_track = None  # Initialize to None

    for artist in artist_search:
        if artist.title.lower() == artist_name.lower():
            print(f"Artist found: {artist.title}")
            print(f"Searching for Track: {track_title} in albums")

            for album in artist.albums():
                album_title = album.title.lower()
                print(f"Processing Album: {album.title}")

                if re.search(r'\b\d{2,4}-\d{1,2}-\d{1,2}\b', album_title) or \
                   'live' in album_title or \
                   'concert' in album_title or \
                   'SBD' in album_title:
                    print(f"Skipping Album: {album.title}")
                    continue  # Skip this album

                print(f"Searching in Album: {album.title}")
                for track in album.tracks():
                    if track.title.lower() == track_title.lower():
                        print(f"Potential Track found: {track.title} in Album: {album.title}")
                        found_track = track  # Update found_track

    if found_track:
        print(f"Final Track found: {found_track.title}")
        return found_track
    else:
        print(f"Track {track_title} not found!")
        return None

for song_name in songs:
    artist, title = song_name.split(' - ')
    track = find_track(artist, title)
    if track:
        items.append(track)
    else:
        print(f"Couldn't find song: {song_name}")

# Create a playlist with the found songs
if items:
    try:
        plex.createPlaylist(playlist_name, items=items)
        print(f"Playlist '{playlist_name}' created successfully.")
    except Exception as e:
        print(f"Error creating the playlist: {e}")
else:
    print("No songs found to create a playlist.")

Expected Behavior

No response

Additional Context

No response

Operating System and Version

Windows 11

Plex Media Server Version

1.32.5.7516

Python Version

3.11.5

PlexAPI Version

4.15.2

armandhammer commented 9 months ago

Here's the contents of playlist.txt -- the first song by REM does not exist in my library. I'd like to know how to find this song on Tidal via Plex and add it to the playlist. Of course I am a Plex Pass and Tidal subscriber.

R.E.M. - E-Bow the Letter
Radiohead - Paranoid Android
Nirvana - Drain You
Pearl Jam - Yellow Ledbetter
Pixies - Wave of Mutilation
JonnyWong16 commented 9 months ago

The python-plexapi library currently doesn't support the Tidal integration.

armandhammer commented 9 months ago

Thanks for the reply. So there's no way to parse the following XML and add the track to a Plex playlist using the python-plexapi? Can I add it via the tidalId? It seems like this should be fairly simple, but I have no idea what to try next since I'm a very novice programmer.

<Hub title="Tracks" type="track" totalSize="20" size="20" attribution="com.tidal" context="hub.search.track" hubIdentifier="search.track">
<Track guid="plex://track/5bf7aea1afe8e5001d1798bf" key="/library/metadata/5bf7aea1afe8e5001d1798bf" ratingKey="5bf7aea1afe8e5001d1798bf" type="track" thumb="http://rovimusic.rovicorp.com/image.jpg?c=qX8hD_gmLcgR1ybdu6g0r5JsLnk-ZzaIKfxTK2XjQMA=&f=0" addedAt="842313600" duration="324000" attribution="com.tidal" attributionLogo="https://provider-static.plex.tv/music/tidal-logo.svg" userState="0" title="E-Bow The Letter" grandparentTitle="R.E.M." grandparentType="artist" grandparentThumb="https://resources.tidal.com/images/598806d2/8866/410e/b7e6/0d724a9d84b0/750x750.jpg" grandparentRatingKey="5b8880eb9d6072003224837f" grandparentGuid="plex://artist/5b8880eb9d6072003224837f" grandparentKey="/library/metadata/5b8880eb9d6072003224837f" parentTitle="New Adventures In Hi-Fi" parentType="album" parentThumb="https://resources.tidal.com/images/8965cb23/efea/4db7/a826/c7eae33008c3/750x750.jpg" parentRatingKey="5bf72cc8d706a2001db42bd2" parentGuid="plex://album/5bf72cc8d706a2001db42bd2" parentKey="/library/metadata/5bf72cc8d706a2001db42bd2" index="5" parentIndex="1" ratingCount="214281" originallyAvailableAt="1996-09-10" year="1996" tidalId="77624891">
<Media container="flac" audioCodec="flac" duration="324605" bitrate="930" optimizedForStreaming="1">
<Part key="/library/parts/77624891-LOSSLESS" container="flac" duration="324605"/>
</Media>
<Media container="mp4" audioProfile="lc" audioCodec="aac" duration="324000" optimizedForStreaming="1">
<Part key="/library/parts/77624891-HIGH" container="mp4" duration="324000" audioProfile="lc"/>
</Media>
<Media container="mp4" audioProfile="he-aac" audioCodec="aac" duration="324000" optimizedForStreaming="1">
<Part key="/library/parts/77624891-LOW" container="mp4" duration="324000" audioProfile="he-aac"/>
</Media>
<Genre filter="genre=5c81401042ed8a0028fb7116" id="5c81401042ed8a0028fb7116" key="/library/sections/tidal/all?genre=5c81401042ed8a0028fb7116" ratingKey="genre_5c81401042ed8a0028fb7116" slug="pop-rock" tag="Pop/Rock" directory="1" context="tag.genre"/>
</Track>
JonnyWong16 commented 9 months ago

It might be possible if you go a bit lower level in the python-plexapi library, but I don't know off the top of my head.

armandhammer commented 9 months ago

Thanks. I still can't get it to add local AND Tidal tracks to the same playlist, but I did get a script working to add only the Tidal versions of tracks to a playlist when reading from a file called playlist.txt and putting the tracks in the format ARTIST - TRACK. Maybe this will help someone. I use ChatGPT to create really great playlists, much better than the "Sonic Sage" that Plex recently added.

You must replace the [MY PLEX TOKEN] and [DEVICE NAME] and the missing variables in the PLEX_SERVER_URL. This can be found in the Plex logs.

import requests
from xml.etree import ElementTree as ET
from urllib.parse import quote

PLEX_TOKEN = '[MY PLEX TOKEN]'
PLEX_SERVER_URL = 'https://192-168-X-XXX.[SERVER STRING].plex.direct:32400'
DEVICE = [DEVICE NAME]

def search_tidal(artist, title):
    query = f"{artist} {title}"
    url = f"https://music.provider.plex.tv/hubs/search?query={quote(query)}&X-Plex-Token={PLEX_TOKEN}"

    response = requests.get(url)
    if response.status_code != 200:
        print(f"Error: Unable to search Tidal: {response.status_code}")
        return None

    tree = ET.ElementTree(ET.fromstring(response.text))
    root = tree.getroot()

    if root is None:
        print("Error: Root is None")
        return None

    track_hub = root.find('./Hub[@type="track"]')
    if track_hub is not None:
        for track in track_hub.findall('./Track'):
            artist = track.get('grandparentTitle')
            track_title = track.get('title')
            tidal_id = track.get('guid').split('/')[-1]

            if artist and track_title and tidal_id:
                return tidal_id

    return None

# Modify the add_track_to_playlist function to accept a list of tidal_ids
def add_track_to_playlist(tidal_ids, playlist_title):
    if not tidal_ids:
        print("Cannot add tracks to a playlist with no Tidal IDs.")
        return

    # Format the list of Tidal IDs as a comma-separated string
    tidal_id_str = "%2C".join(tidal_ids)

    create_playlist_url = f"{PLEX_SERVER_URL}/playlists?title={quote(playlist_title)}&smart=0&type=audio&uri=provider%3A%2F%2Ftv.plex.provider.music%2Flibrary%2Fmetadata%2F{tidal_id_str}&includeFields=thumbBlurHash&X-Plex-Device-Name={DEVICE}"

    response = requests.post(create_playlist_url, headers={'X-Plex-Token': PLEX_TOKEN})

    if response.status_code == 201:
        print(f"Tracks added to the '{playlist_title}' playlist successfully.")
    else:
        print(f"Response adding the tracks to the playlist: {response.status_code}")

# Prompt for the playlist name
playlist_name = input("Enter the Playlist Name: ")

# Read the track list from "playlist.txt" and process each line
with open("playlist.txt", "r") as file:
    tidal_ids = []  # Store the found Tidal IDs
    for line in file:
        artist, track_title = line.strip().split(" - ")
        tidal_id = search_tidal(artist, track_title)
        if tidal_id:
            tidal_ids.append(tidal_id)  # Add Tidal ID to the list
        else:
            print(f"No Tidal track found for '{artist} - {track_title}'.")

# Call the modified add_track_to_playlist function with the list of Tidal IDs
add_track_to_playlist(tidal_ids, playlist_name)

print("All tracks processed.")
JonnyWong16 commented 9 months ago

Closing this issue because there is a feature request to support Tidal integration.

671