Skip to content

Plex Support #531

@ShanaryS

Description

@ShanaryS

Is your feature request related to a problem? Please describe.
When using Plex, we need to manually use a subtitle file from disk, instead using the ones with the Plex media.

Describe the solution you'd like
Automatic detection of subtitles when using Plex, similar to Emby and Jellyfin.

Describe alternatives you've considered
None.

Additional context
I'm not familiar with extension development but I was looking into how this can be done. Unfortunately, it seems plex doesn't have the ApiCilent like Emby/Jellyfin which makes it harder.

I did see the the request plex uses for the subtitles by monitoring network when switching the subtitles. The request with the minimally required params:
https://127.0.0.1:32400/video/:/transcode/universal/subtitles?path=%2Flibrary%2Fmetadata%2F51494&protocol=dash&session=abc&X-Plex-Platform=Chrome&X-Plex-Token=abc
Removing protocol=dash will load the entire subtitle, albeit slowly. Plex seems to only provide the subtitles as far as the video is buffered with dash enabled. /library/metadata/123 is just the ratingKey for the media, which is Plex's unique id for content at least at a per library level. Since this requires session, it will only get the subtitle currently selected. I don't believe there is a way to get all but this isn't a big deal.

The only problem here getting a token which I believe there are two choices. Ask the user for a plex token that asbplayer stores, this is how apps usually interact with plex. This would work for non admin users since anyone can monitor their request traffic to get it. The alternative is asbplayer monitoring the requests on the page which I think is possible but I'm not sure how that will affect the extensions permissions.


I've done some more research into this. All external subtitles can be downloaded directly at any time using https://127.0.0.1:32400/library/streams/123&X-Plex-Token=abc where 123 is the subtitle stream id. We only need the token and ratingKey for external subtitles. All metadata is stored for each subtitle as well.

However internal subtitles can only be downloaded when it's in an active session and not being burned in. Luckily the selected boolean is stored with the subtitle metadata and the user can disable burn-in in their plex settings (for non-image formats). Internal subtitles requires token, ratingKey, and session.

In short, this is definitely possible and fairly straight forward, but the concern is how to get the token, ratingKey, and session. I'm not familiar with extensions so I don't know how to dynamically get this data, though the token can probably just be a setting.


I got this working with hardcoded values. This allows external subtitles to be accessed. But internal subtitles need session which changes for each stream and everytime something is changed (e.g user selects a different subtitle or quality).

Working implementation for plex-page.ts. The only thing left to do is get the four variables at the top from the page, but I don't know how to do it.

import { VideoDataSubtitleTrack } from '@project/common';
import { VideoData } from '@project/common';
import { trackFromDef } from './util';

const serverUrl = ""
const ratingKey = ""
const token = "";
const session = "";

document.addEventListener(
    'asbplayer-get-synced-data',
    async () => {
        const response: VideoData = { error: '', basename: '', subtitles: [] };
        const res = await fetch(`${serverUrl}/library/metadata/${ratingKey}?X-Plex-Token=${token}`);
        const xmlText = await res.text();
        const parser = new DOMParser();
        const xmlDoc = parser.parseFromString(xmlText, 'application/xml');

        const metadata = xmlDoc.querySelector('Video');
        if (!metadata) {
            response.error = 'No metadata found for Plex video';
            return document.dispatchEvent(
                new CustomEvent('asbplayer-synced-data', {
                    detail: response,
                })
            );
        }
        response.basename = metadata.getAttribute('title') ?? '';

        const subtitles: VideoDataSubtitleTrack[] = [];
        const parts = metadata.querySelectorAll('Part');
        parts.forEach(part => {
            const streams = part.querySelectorAll('Stream[streamType="3"]');
            streams.forEach(stream => {
                const streamKey = stream.getAttribute('key');
                if (streamKey) {
                    // Only external can be downloaded directly
                    subtitles.push(
                        trackFromDef({
                            label: stream.getAttribute('extendedDisplayTitle') ?? '',
                            language: stream.getAttribute('language') ?? '',
                            url: `${serverUrl}${streamKey}?X-Plex-Token=${token}`,
                            extension: stream.getAttribute('codec') ?? '',
                        })
                    );
                    return;
                }
                if (stream.getAttribute('selected') === '1') {
                    // Internal can only be when transcoding and not burned in. Url request will fail if burned in, user can turn off burn in.
                    subtitles.push(
                        trackFromDef({
                            label: stream.getAttribute('extendedDisplayTitle') ?? '',
                            language: stream.getAttribute('language') ?? '',
                            url: `${serverUrl}/video/:/transcode/universal/subtitles?path=%2Flibrary%2Fmetadata%2F${ratingKey}&session=${session}&X-Plex-Platform=Chrome&X-Plex-Token=${token}`,
                            extension: stream.getAttribute('codec') ?? '',
                        })
                    );
                }
            });
        });
        response.subtitles = subtitles;

        document.dispatchEvent(
            new CustomEvent('asbplayer-synced-data', {
                detail: response,
            })
        );
    },
    false
);

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions