Page 1 of 1

playlist export via Perl

PostPosted: Mon Jun 19, 2017 5:22 pm
by Jägs
Greetings:

Thought I'd share my Perl playlist exporter for Subsonic, in case anyone might find it useful. It's been a while since I wrote it, but I think I relied heavily upon the Java version posted in these discussion. It's also a bit hacky, as I hadn't programmed in Perl for quite some time, so there's likely a bunch of stuff that can be cleaned up, but here you go:

Code: Select all
#!/usr/bin/perl

use HTTP::Request::Common qw(POST);
use LWP::UserAgent;
use HTML::Strip;
use Class::Struct;
use Switch;

# create the structure that will be used to store the playlist information
struct ( playlist => {
      playlistID => '$', # DB ID of the playlist
      playlistName => '$', # name of the playlist
      playlistSongs => '@', # array of songs in the playlist
      }
   );

# create the structure that will be used to store the song information
# this will be used to generate the M3U format
struct ( song => {
      songPath => '$', # path of the song
      songArtist => '$', # artist
      songTitle => '$', # title
      songDuration => '$', # duration in seconds
      }
   );

# this will be an array containing instances of the structure we created above
my @playlists;


if ( $#ARGV + 1 != 4 ) {

   print "Usage:  playlistExport.pl <URL to db.view> <user:password base64 encoded> <format (bak or  m3u)> <playlist path>\n\n";
   exit;
}

else {
   my $baseURL = $ARGV[0];
   my $playlistQuery = "select ID, NAME from PLAYLIST;";
   my $songQuery = "SELECT PATH, ARTIST, TITLE, DURATION_SECONDS FROM MEDIA_FILE INNER JOIN PLAYLIST_FILE ON (MEDIA_FILE.ID = PLAYLIST_FILE.MEDIA_FILE_ID) WHERE PLAYLIST_ID= XX ORDER BY PLAYLIST_FILE.ID;";
   my $authEncoded = $ARGV[1];
   my $playlistFormat = $ARGV[2];
   my $playlistPath = $ARGV[3];

   # if the user did not add the trailing slash, let's add it
   if ( $playlistPath !~ /\/$/ ) {
      $playlistPath .= "/";
   }
   
   my $userAgent = LWP::UserAgent->new();
   
   # first, get the playlists
   my $request = POST $baseURL,
      Content_Type => form-data,
      Content => [ 'query' => $playlistQuery ],
      Authorization => 'Basic ' . $authEncoded;
   
   my $playlistHTML  = $userAgent->request($request)->as_string();

   print "playlistHTML = $playlistHTML\n";
   
   # now, parse through the raw playlist HTML code and store the playlist names and IDs in an array
   
   my $currentIndex = -1;
   
   for (split /^/, $playlistHTML) {
   
      if ( $_ =~ 'ruleTableCell' ) {
   
         # set up the tag stripper
         my $stripper = HTML::Strip->new();
      
         # strip the HTML tags/attributes
         my $thisData = $stripper->parse($_);
   
         # remove any leading/trailing whitespace
         $thisData =~ s/^\s+|\s+$//g;
   
         # if the remaining data is entirely numeric (the ID), set the the array index to it
         if ( $thisData =~ /^\d+$/ && $currentIndex == -1 ) {
            $currentIndex = $thisData;
         }
         
         # otherwise, it should be the playlist name, so push it to the array and reset the index
         else {
            my $thisPlaylist = playlist->new ( playlistID => $currentIndex, playlistName => $thisData );
            push (@playlists,  $thisPlaylist);
            $currentIndex = -1;
         }
      }
   }
   
   # now, for each of the playlists, grab the list of songs
   for ( $indexer = 0; $indexer <= $#playlists; $indexer++ ) {
      
      # set up the query to get the songs from the playlist
      my $thisQuery = $songQuery;
      my $thisID = $playlists[$indexer]->playlistID;
      $thisQuery =~ s/XX/$thisID/;

      # now, get the songs for the given playlist
      my $request = POST $baseURL,
         Content_Type => form-data,
         Content => [ 'query' => $thisQuery ],
         Authorization => 'Basic ' . $authEncoded;
   
      my $songsHTML  = $userAgent->request($request)->as_string();

      # this will be used to keep track of the current field, so we can populate the song array with the proper information
      my $currentField = 0;
      my $thisSong;

      # just like the playlists, we need to parse through the $songsHTML to get the paths and add them to the playlist's array
      for (split /^/, $songsHTML ) {
   
         if ( $_ =~ 'ruleTableCell' ) {

            # set up the tag stripper
            my $stripper = HTML::Strip->new();
      
            # strip the HTML tags/attributes
            my $thisData = $stripper->parse($_);

            # remove any leading/trailing whitespace
            $thisData =~ s/^\s+|\s+$//g;

            switch ( $currentField ) {
      
               case 0   { $thisSong = song->new (songPath => $thisData); $currentField++; }
               case 1   { $thisSong->songArtist($thisData); $currentField++; }
               case 2   { $thisSong->songTitle($thisData); $currentField++; }
               case 3   { $thisSong->songDuration($thisData); push (@{$playlists[$indexer]->playlistSongs}, $thisSong); $currentField = 0; }
            }
         }
      }
   
      # now, output the playlists to the the format and path specified by the user

      # first, check to see if the path exists
      if ( -d $playlistPath ) {

         # the path exists, so let's create the file handler
         open (THISPLAYLIST, ">" . $playlistPath . $playlists[$indexer]->playlistName . "." . $playlistFormat ) or die "ERROR:  could not open the playlist file (" . $playlists[$indexer]->playlistName . ") for writing!\n";

         print "processing playlist:  " . $playlistPath . $playlists[$indexer]->playlistName . "." . $playlistFormat . "\n";

         # if the user has specified M3U as the format, print out the header
         if ( $playlistFormat eq "m3u" ) {
            print THISPLAYLIST "#EXTM3U\n";
         }
         
         # now, loop through all of the songs in the playlist, and print them to the file in the specified format
         foreach ( @{$playlists[$indexer]->playlistSongs} ) {
            
            switch ($playlistFormat) {

               case "m3u" { print THISPLAYLIST "#EXTINF:" . $_->songDuration . ", " . $_->songArtist . " - " . $_->songTitle . "\n" . $_->songPath . "\n"; }
               case "bak" { print THISPLAYLIST $_->songPath . "\n";}
            }
         }

         # close the file
         close (THISPLAYLIST);
      }

      else {
         print "ERROR:  the specified path ($playlistPath) does not exist!\n";
         exit;
      }
   }
}


Here's the usage:

Code: Select all
./playlistExport.pl (URL to Subsonic's db.view) (encoded Auth ID) (playlist format) (export path)


I use this in cron to run nightly to create weekly backups, so here's my cron script

Code: Select all
FOLDERID=`date +\%w`; /path/playlistExport.pl http://localhost/music/db.view XXXXXXXXXXXXXXXX m3u /path/$FOLDERID; touch /path/$FOLDERID

Re: playlist export via Perl

PostPosted: Thu Sep 14, 2017 7:44 pm
by MikeLegit
Can you explain how to obtain the (encoded Auth ID)? So far I have found this string in subsonic.script, however using enc:2159373669417521 gives me the empty username error below.

[/share/CACHEDEV1_DATA/.qpkg/QSubSonic] # cat db/subsonic.script |grep mike
INSERT INTO USER VALUES('mike','enc:2159373669417521',169449424182,243543408,0,FALSE,'myemail@gmail.com')

Code: Select all
# ./playlistExport.pl http://192.168.1.5:4040/db.view enc:2159373669417521 m3u /root/3/playlist
Unquoted string "form" may clash with future reserved word at ./playlistExport.pl line 54.
Unquoted string "playlist" may clash with future reserved word at ./playlistExport.pl line 86.
Unquoted string "form" may clash with future reserved word at ./playlistExport.pl line 103.
Unquoted string "song" may clash with future reserved word at ./playlistExport.pl line 129.
Argument "data" isn't numeric in subtraction (-) at ./playlistExport.pl line 53.
Argument "form" isn't numeric in subtraction (-) at ./playlistExport.pl line 53.
playlistHTML = HTTP/1.1 401 Empty Username
Connection: close
Server: Jetty(6.1.x)
WWW-Authenticate: Basic realm="Subsonic"
Content-Length: 1389
Content-Type: text/html; charset=iso-8859-1
Client-Date: Thu, 14 Sep 2017 19:53:06 GMT
Client-Peer: 192.168.1.5:4040
Client-Response-Num: 1
Title: Error 401

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
<title>Error 401 </title>
</head>
<body><h2>HTTP ERROR: 401</h2><pre>Empty Username</pre>
<p>RequestURI=/db.view</p><p><i><small><a href="http://jetty.mortbay.org/">Powered by Jetty://</a></small></i></p><br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

</body>
</html>



-ML

Re: playlist export via Perl

PostPosted: Tue Oct 03, 2017 5:16 pm
by Jägs
Sorry, just saw your post. I was revisiting because I had to reinstall my server and needed to grab the script.

For the AuthID, just take your username and password, with a colon in between them, and run it through Base64 encoding. You can use this website, if you want:

https://www.base64encode.org/

So, if my username is "user," and my password is "password," I would plug in "user:password" into the site above, which would generate:

dXNlcjpwYXNzd29yZA==

Then you should be good to go. I've updated the script above to be more clear on this.