jc21 / plex-api

PHP API for Plex Servers
Other
35 stars 8 forks source link

Documentation is sorely needed #14

Open delovelady opened 2 years ago

delovelady commented 2 years ago

While this API has wonderful promise, based on the overview, lack of documentation makes it very difficult if not impossible to use practically.

Case in point, the following code:

$filter1 = new Filter('title', $favoriteTitle) ;
foreach($librarySections['Directory'] as $directoryNumber => $directoryAttributes) {
    ($type = $directoryAttributes['type'] != 'movie')
        continue ;
    $sectionNumber = $directoryAttributes['key'] ;
    $res = $client->filter($sectionNumber, [$filter1], true) ;  // Note: This results in three warnings about Video not being an index in jc21/plex-api/src/jc21/PlexApi.php on line 405
    if ($res) {
        foreach ($res as $movie) {
            $a=$movie->title ;
            // how to access the elements of movie?
                //foreach ($movie->data as $element) {} // Exception Return value of jc21\Movies\Movie::__get() must be an instance of jc21\Movies\mixed, null returned
                //foreach ($movie as $element) {} // Loop does not get entered
                //$title = $movie['title'] ; Exception Cannot use object of type jc21\Movies\Movie as array
                //$title = $movie->title : Exception TypeError: Return value of jc21\Movies\Movie::__get() must be an instance of jc21\Movies\mixed, string returned
                //$movie->toArray() results in undefined function
                //$array = $movie->jsonSerialize() ; Exception Return value of jc21\Movies\Movie::jsonSerialize() must be an instance of jc21\Movies\mixed, array returned
            }
        }

It's not at all clear where to go once the $movie object has been created. Here's the result of the $title = $movie->title effort:

Screen Shot 2022-08-29 at 9 30 24 AM
delovelady commented 2 years ago

It turns out that some of these errors are the result of trying to run this API at PHP version 7.4.3. Is it intended that PHP 8.0+ be required? (The use of :mixed as in __get(string $var): mixed above was introduced in PHP8.) To make this usabile in PHP7, the :mixed must be removed... but it must be left in the doc block.

This does not remove the need for documentation; on the contrary, it amplifies the need. Limits and usage are also key to understanding how to use this product.

godsgood33 commented 2 years ago

@delovelady I was the one that wrote the last push and update. I never tested the filters because I didn't need them for my use. If you look at the PR you'll see I did write a bunch of docs for the new functionality I pushed. If you could give me a little more info on your use-case I'll see if I can work on a fix for the filters. I have removed the : mixed from all the files I committed so that error should go away with the next release.

From what I can tell $res should be a ItemCollection. So $movie should be a Movie object, but it seems that the filter isn't returning correctly. If we can troubleshoot what filter is returning that should fix it.

godsgood33 commented 2 years ago

The 3rd and 4th lines of your code doesn't make sense to me. It looks like you are trying to skip any directory that isn't of type movie, but I think that line is having the opposite effect... since copying the code directly throws a syntax error I guessed that you meant the following...

($type = $directoryAttribute['type'] != 'movie');
if (!$type) {
    continue;
}

And you're right, this yields the same error you seeing (1 for each library of type 'movie'). If I remove the ! in the if block the rest of the code works just fine. Now the filter works and it only throws 2 errors (I have 3 'movie' libraries too). And I agree, that should not throw an error. If it were me, I would probably refactor the code to be a little more readable, and I'd put in checks before filtering further to only filter certain libraries. For example, I have 2 family libraries so I can filter those out like this

if ($directoryAttributes['type'] != 'movie') {
    continue;
}

if ($directoryAttributes['agent'] == 'com.plexapp.agents.none') { //will filter out personal libraries
    continue;
}

$sectionNumber = $directoryAttributes['key'];
$res = $client->filter($sectionNumber, [$filter1], true);
if (is_a($res, 'jc21\Collections\ItemCollection')) {
    foreach ($res as $movie) {
        print $movie->title.PHP_EOL; // prints the title of the filtered movie

Once we figure out this issue you're running into I'll attempt to help with the error itself and fix that code.

godsgood33 commented 2 years ago

@delovelady Hey, just wanted to follow up and see if you needed any other help or was my explanation sufficient?

delovelady commented 2 years ago

@godsgood33 Oops, so sorry, Ryan... this message and the others hit my spam folder. (Fixed now.)

Well, I can tell you immediately that that my line 3 was missing an all-important "if", but further test time will be required to go beyond that.

Wow, you've done a lot for me here. THANK YOU so much, and I promise to get back to this and follow up in the coming week. (I actually didn't really expect any response, so your gracious extra effort here is special for me. THank you again!)

Sorry I left you hanging. Won't happen again.

delovelady commented 2 years ago

Oh, but the specific use case above: I hope to write a script with which I can take a movie that was watched (it'll be in the TV or NewMovies or SaveForQM library), and move its files to the folder for Movies. Also, depending on switches set in the command line, possibly link those files in the FavoritesToShare library (containing stuff I'd like to share with friends).

So in general, the command line would specify a title. The script would identify that title and its location. If more than one match, print an error and exit. If not found, or not in a correct library, print an error and exit. Otherwise, move folder and files to another folder, and potentially create the stuff for the FavoritesToShare folder.

delovelady commented 2 years ago

And I know (now) you didn't write the filter stuff... but in case you're interested I've found problems with that also. Especially the fact that it seems to like only one-word searches. It's a problem, for example, with "Spider Man," of which I have a few iterations. "Spider Man" reveals no matches (I have Spider Man I, Spider Man II, Spider Man III, etc.). But Spider matches all three (four, actually). I presume I could add a filter for each word, but that seems odd... shouldn't the other "just work?" Goes back to that documentation. I don't know what's broken and what I'm expecting too much from.

godsgood33 commented 2 years ago

@delovelady I'm in the process of adding some unit tests to further support future versions.

So you want to look in your Plex library for movies you've watched and possibly move them to a different folder (within the same movie library or in a different Plex library?). I guess that "TV", "NewMovies", and "SaveForQM" are library names which means they are probably different folders. What if you were to put the script on a scheduled/cronjob and then base your decision on some other factors like your rating of the movie, special tags, or other factor, then it would just automatically do the work. Then presuming your friends (and the library/server) has notifications turned on, they'd see any changes.

The filters may just need an encoding transform (adding something like "Spider%20Man" or "Spider+Man"). Run that filter on your Plex server and look at the AJAX packet to see if it transforms the request, then run that same transform on your script.

godsgood33 commented 2 years ago

@delovelady Filters and search with this API must be exact (except for case sensitivity). Most Spider-Man movies use the '-' between the two words, so using a space doesn't work. It appears as though Plex is doing some character matching analysis in the backend when searching for some things because "searching" for "spider man" does show the movies, but filtering does not.

If you have any suggestions for the documentation you allege is so lacking, please, share.

delovelady commented 2 years ago

Hi, Ryan @godsgood33 :

This is working for me now:

// Here, we'll get all the "sections" (which are the key# for each Library) from the system
// We're particularly interested in General (a.k.a. Movies) and 'Share Favorites'.
// And we'll also see if any titles match the request, in any section.  Record the section for
// each match into $foundTitleSections[]
foreach ($librarySections['Directory'] as $directoryNumber => $directoryAttributes) {
    if ($type = $directoryAttributes['type'] != 'movie')
        continue ;
    $sectionNumber = $directoryAttributes['key'] ;
    $libraryName = $directoryAttributes['title'] ;
    $filteredList = $client->filter($sectionNumber, $filterArray, true) ;
    if ($libraryName == GENERAL_TITLE) {
        $generalSection = $sectionNumber ;
        $generalPath = $directoryAttributes['Location']['path'] ;
        }
    elseif ($libraryName == FAVORITES_TITLE) {
        $favoritesSection = $sectionNumber ;
        $favoritesPath = $directoryAttributes['Location']['path'] ;
        }
    foreach ($filteredList as $key => $movie) {
        $title = $movie->title ;
        $file = $movie->media->path ;
        if ( ! is_file($file)) { // File was removed since last library update
            unset($filteredList[$key]) ;
            continue ;
            }
        $foundEntries[] = [
                              'section' =>  $sectionNumber
                            , 'library' =>  $libraryName
                            , 'title' =>    $title
                            , 'filePath' => $file
                            , 'year' =>     $movie->year
                                ] ;
        if ($sectionNumber == $generalSection)
            $isInGeneralLibrary = true ;
        elseif ($sectionNumber == $favoritesSection)
            $isInFavorites = true ;
        }
    }

$matchCount = count($foundEntries) ;
if ($matchCount < 1) {
    printf("Title not found in the Plex libraries.\n") ;
    exit(1) ;
    }
$prevLibrarySection = null ;
foreach ($foundEntries AS $entry) {
    if ($entry['section'] != $prevLibrarySection) {
        $prevLibrarySection = $entry['section'] ;
        printf("Found in library '%s':\n", $entry['library'] ) ;
        }
    printf("    %s\n", $entry['title'] . (isset($entry['year']) ? ' (' . $entry['year'] . ')' : '')) ;
    }
if ($matchCount > 1) {
    printf("%d matching titles were found.  Limit is 1.\n", $matchCount) ;
    exit(1) ;
    }
if ($isInFavorites) {
    printf("Title already exists in Favorites.  Not added.\n") ;
    }
else {
    ...

I am not (yet, if ever) so organized that I could use the personal libraries and ratings suggestions --- but I do like those very much - especially the rating one. In fact, I'm looking into what it will take to make your suggestion fit our Method of Operation. Advantage would be that the title filter search could be eliminated in favor of something nicer. Plus the hands-off aspect. I will look into it.

My results of working with filters - and coming up with the idea that they seem to be single-word filters - was based on more than just Spider-Man, but I cannot now give you better examples. As you say, some behind-the-scenes may have thrown me off. The reasoning was also based on the fact that the example filter (/jcw1/plex-api/docs/Filter.md) mentions "george" when looking for "George of the Jungle."

You correctly surmised that my goal was to move from one folder to another. I have these sections, titles and paths:

 0 Cocomelon            /Media/Cocomelon
 1 DVD Conversions      /Media/Converted-DVDs
 2 Lovelady creations   /Media/MyVideos
 3 Movies               /svr/PlexMedia/Movies
 4 New Movies           /Media/NewStuff
 5 Save for Qiu Min     /Media/QiuMin
 6 Share Favorites      /Media/Favorites-to-Share
 7 Tablo                /Media/Tablo
 8 Title Tester         /svr/PlexMedia/Title Tester
 9 TV Conversions       /Media/TV-DVDs-to-Review
10 TV Shows             /Media/TV
11 Music                /Media/MusicPlex

I might want to move something from section 4 to section 3 and add to section 6.

I can be abrasive unintentionally. (And e-"communication" can triple that!) I didn't mean to hit a nerve with my documentation comments. Please accept my apologies. Here is a list of headscratchers for me (I have come to terms with some of my questions by investigation, and time has made me forget.... so the list goes on).

For starters: the 'using' section of README is limited to this:

use jc21\PlexApi;
$client = new PlexApi('192.168.0.10');
$client->setAuth('username', 'password');
$result = $client->getOnDeck();
print_r($result);

It would be nice to, at least, mention that other use statements are required for certain functions. For example 'use jc21/Util/Filter' is needed in order to use the filters. (Maybe it's unusual (?) but "use" is a new concept for me. I was lost for while, trying to figure out how to implement a filter.) Also, maybe I'm missing something since you seem surprised that I think documentation is missing. Perhaps if I rephrased: Where do I find the documentation, and how do I access it? I assumed it was just the README.md in the project description. That said, I did dig into the source and found some documentation files (*.md) from which I was able to glean some information. But I don't understand the format, and google "github .md files" doesn't seem to help me out. So I remain lost.

Why is the WIKI completely empty?
What are the components of a jc21\Collections\ItemCollection (Maybe I just don't know how to deal with .md files?)
What are the components returned by getLibrarySections()

Again, for some of this I've satisfied my most pressing questions for now. (Thank God for graphic visual editors! Without VSCODE I would not have been able to begin work with this.) The tool is clearly very powerful and useful, and clearly represents great effort. I don't mean to come down hard on it. My frustrations can get the better of me.

I believe that should tie up any remaining questions about my original post. I have also run into some inconsistency about the initial login. But I see an issue (4 years old: "https://github.com/jc21/plex-api/issues/8") that mentions the very problem I have had. I had to put the login into a loop, retrying until ($token = $client->getToken()) returns something that doesn't evaluate as false. Sometimes it'll hit on the first try, I've had it fail with as many as 10 tries (my limit) but next iteration works on first (or second or ...) try But again, I'll add to that issue (assuming I can).

Thanks for your time.

godsgood33 commented 2 years ago

That's an intriguing use case. I'd be curious to look at your code if you ever want to make it public.

Maybe I was being to shortsighted in adding the example for the Filter class...in my mind it functions as AJAX filtering object, so a user would be live typing into an autocomplete box that would filter the results and return with the matches, so that's why the one word.

The 'using' section is short because many of the examples were moved to the other markdown files and because it is intended as a 'getting started'. The implementing developer has to then do the work to see what other methods are available and how to implement those for his/her use case. When developing a library like this it can be hard to write documentation to fit enough use cases to give everybody a big picture understanding of how to best use it.

The 'use' statements are required because an autoloader is being used. The alternative would be you'd have to require_once ('Util/Filter.php'); in every file that has a 'Filter' object. The benefit is that with modern IDE's they can detect that and put the use... statement in for you automatically. If you're using VSCode then I'd recommend the 'PHP Intelephense' plugin and it will do just that. I did leave the Filter object as optional for the filter method...you can just implement an array. For example, these should return the same thing (I did this for backwards compatibility):

$res = $api->filter($sectionKey, [['title' => 'Spider-man']]);

$filter = new Filter('title', 'Spider-man');
$res = $api->filter($sectionKey, [$filter]);

The main documentation is in the docs directory start here. Again, if you're using VSCode, I'd recommend a markdown editor plugin like 'Markdown All in One', it can help you see what you're doing by rendering a preview on an adjacent tab. And if you leave the preview open on one side, and the code you're working on next to it you can reference the docs.

I don't know why the wiki is empty, I usually focus on markdown docs as they are easier to format and keep up to date as they are part of the code base. It's so easy to forget about something like a wiki that it could easily go out of date.

ItemCollection just implements the IteratorAggregate interface so it has to implement those methods (count(), getData(), addData(), & getIterator()). Typically the getIterator method is only called when you use something like a foreach or while loop, those automatically create an iterator to loop through the collection (foreach ($collection as $item)). Collections are a OO way of dealing with arrays of objects. I used this dev.to article to help me understand collections and iterators. I ended up asking a question and he answered it in a subsequent post if you scroll down to the comments.

If you look at the PlexApi.md doc, the getLibrarySections method returns an array. Basically what is happening is the API is doing a curl (HTTP GET/POST) request to the server and requesting information, the server responds with XML (you can see these responses if you visit the URL below). The API then reformats this XML to an array with the xml2array() method.

https://{PLEX SERVER IP}:32400/library/sections/?X-Plex-Token={Your PLEX TOKEN}

To simplify things, it may be helpful to use something like a .env file to store values that you want to use, but for security reason's, don't commit those to your repo if you have one. Yeah, I ran into that same problem, then I started storing the token and using that instead...works faster (since it doesn't have to run another curl request to plex.tv), and is more secure since I don't have to keep my username and password in the library.

godsgood33 commented 1 year ago

The latest commit should fix your issues, let me know if you have any other issues. If you don't we can close this and #13.