#!/usr/bin/perl # Written by Paul Rogers. (C) 2008 and All Rights Reserved. # # This is open source and can be re-used, modified and included in other works, providing # credit is given to the original author on code or ideas used. # # I also make no guarantees or warranties on this program - you use it at your own risk. # # The Perl library MP3::Tag needs to be installed so tags can be read / written to. # # the_uhu@hotmail.com use MP3::Tag; use Getopt::Long; use Encode; $_ = 1; $optCreateLinks = 0; $optDebug = 0; $optLinksDir = ""; $optMP3Dir = ""; $optHelp = 0; $optDelNonExists = 0; $optSkipErrs = 0; GetOptions( 'createlinks' => \$optCreateLinks, 'debug' => \$optDebug, 'delnonexists' => \$optDelNonExists, 'excludemp3dir=s' => \$optExcludeDirs, 'help' => \$optHelp, 'linksdir=s' => \$optLinksDir, 'mp3dir=s' => \$optMP3Dir, 'recreate' => \$optRecreate, 'recursive' => \$optRecursive, 'skiperrs' => \$optSkipErrs); if ($optHelp) { &funcDisplayHelp(); exit (0); } $strLinksDir=""; @glbArrDirList; @glbArrMP3Dir; @glbArrExcludeDirs; $glbIntDirPerms = 775; $glbIntFilePerms = 664; $glbIntMP3Cnt=0; $glbIntM3UCnt=0; if ($^O eq "MSWin32") { $strDirSep = "\\"; &funcPrintMsg(3, "Sorry, this script is not currently designed to run on Windows.\nFeel free to update it yourself!"); exit(1); } else { $strDirSep = "/"; } if ($optMP3Dir) { my $tmpStrMP3Dir = ""; my $tmpIntK = 0; @glbArrMP3Dir = split(/:/,$optMP3Dir); foreach $tmpStrMP3Dir (@glbArrMP3Dir) { $tmpStrMP3Dir =~ s/\\+$//; $tmpStrMP3Dir =~ s/\/+$//; if ($tmpStrMP3Dir =~ m/^\./) { &funcPrintMsg(3, "--mp3dir must contain a full path and not one based on the current directory."); exit(1); } if ($tmpStrMP3Dir !~ m/$strDirSep$/) { $tmpStrMP3Dir = $tmpStrMP3Dir.$strDirSep; } $glbArrMP3Dir[$tmpIntK] = $tmpStrMP3Dir; $tmpIntK++; } } else { &funcPrintMsg(3, "You must supply a directory with the option --mp3dir otherwise we don't know where to scan"); exit(1); } if ($optLinksDir) { $optLinksDir =~ s/\\+$//; $optLinksDir =~ s/\/+$//; $strLinksDir=$optLinksDir.$strDirSep; } else { &funcPrintMsg(3, "You must supply a directory with the option --linksdir otherwise we don't know where to create the links"); exit(1); } if (!$optRecursive && $optExcludeDirs) { &funcPrintMsg(3, "--excludemp3dir can only be used with --recursive."); exit(1); } if ($optExcludeDirs) { my $tmpStrDir = ""; my $tmpIntK = 0; @glbArrExcludeDirs = split(/:/,$optExcludeDirs); if ($optDebug) { &funcPrintMsg(0, "Exclude Dir variable from the command line is: $optExcludeDirs"); } foreach $tmpStrDir (@glbArrExcludeDirs) { $tmpStrDir =~ s/\\+$//; $tmpStrDir =~ s/\/+$//; if ($tmpStrDir =~ m/^\./) { &funcPrintMsg(3, "--mp3dir must contain a full path and not one based on the current directory."); exit(1); } if ($tmpStrDir !~ m/$strDirSep$/) { $tmpStrDir = $tmpStrDir.$strDirSep; } $glbArrExcludeDirs[$tmpIntK] = $tmpStrDir; if ($optDebug) { &funcPrintMsg(0, "Exclude Dir on the array is: $tmpStrDir"); } $tmpIntK++; } } if ($optRecreate && !$optCreateLinks) { &funcPrintMsg(3, "--recreate can only be used with --createlinks."); exit(1); } if (!$optDelNonExists) { &funcPrintMsg(2, "Will NOT remove links for non-existent files. Use --delnonexists switch if otherwise required."); } if (!$optCreateLinks) { &funcPrintMsg(2, "Will NOT be creating any folders for artists / albums and soft links for tracks found."); } if ($optSkipErrs) { &funcPrintMsg(2, "Errors WILL be skipped."); } else { &funcPrintMsg(2, "Errors will NOT be skipped."); } if ($optDebug) { my $tmpIntK = 0; foreach my $tmpStrDir (@glbArrMP3Dir) { &funcPrintMsg(0,"MP3Dir[$tmpIntK]: $tmpStrDir"); $tmpIntK++; } &funcPrintMsg(0,"strLinksDir: $strLinksDir"); } # Read list of *.mp3 and *.m3u files from the directories passed into $optMP3Dir &funcSearch4MP3s(); # Open each file and pull Album Artist, Album Title, Track Number, Track Title and Filename # with a unique ID number into an array. @glbArrMP3List; $glbIntUniqID = 1; &funcPrintMsg(1, "Reading ID3v2 tags..."); foreach $tmpStrFile (@glbArrDirList) { my $tmpIntType = 99; $#glbArrMP3Info = -1; if ( $tmpStrFile =~ /\.mp3$/ ) { if ($optDebug) { &funcPrintMsg(0,"Reading ID3v2 tags from $tmpStrFile"); } $objMP3 = MP3::Tag->new($tmpStrFile); $objMP3->get_tags(); if (exists $objMP3->{ID3v2}) { $objMP3ID3v2 = $objMP3->{ID3v2}; } else { &funcPrintMsg(2, "$tmpStrFile does not have any ID3v2 tags - skipping"); next; } ($tmpStrAlbumArtist, $tmpStrLong) = $objMP3ID3v2->get_frame("TPE2"); ($tmpStrAlbumTitle, $tmpStrLong) = $objMP3ID3v2->get_frame("TALB"); ($tmpStrTitle, $tmpStrLong) = $objMP3ID3v2->get_frame("TIT2"); ($tmpStrTrackNum, $tmpStrLong) = $objMP3ID3v2->get_frame("TRCK"); # This is what you use # (@tmpHashAPIC, $tmpStrLong) = $objMP3ID3v2->get_frame("APIC"); # # It returns a hash within an array like: # # ( { "Description" => "", # "MIME Type" => "image/jpeg", # "Picture Type" => "Cover (front)", # "_Data" => "..data of jpeg picture (binary).." # }, # “Attached Picture" # ); $objMP3->close(); if ($tmpStrAlbumArtist eq "") { &funcPrintMsg(2, "$tmpStrFile does not have an Album Artist set (frame TPE2) - skipping."); next; } if ($tmpStrTrackNum eq "") { $tmpStrTrackNum = "0"; } $tmpIntType = 1; } elsif ( $tmpStrFile =~ /\.m3u$/ ) { # Nasty hack because PERL is treating the foreign chars as non-UTF8 - intended apparently in filenames from readdir. # Need to utf8::decode on all of the parts. See the URL below for more info. # http://perldoc.perl.org/perlunicode.html#When-Unicode-Does-Not-Happen my $tmpStrFileWOPath = &funcRemovePathFromFilename($tmpStrFile); @tmpArrPlaylist = split(/ - /, $tmpStrFileWOPath); $tmpStrAlbumArtist = $tmpArrPlaylist[0]; utf8::decode($tmpStrAlbumArtist); $tmpStrAlbumArtist =~ s/\s+$//; $tmpStrAlbumTitle = ""; for ($tmpIntI = 1; $tmpIntI < @tmpArrPlaylist; $tmpIntI++) { if ($tmpIntI > 1) { $tmpStrAlbumTitle .= " - "; } $tmpStrAlbumTitle .= $tmpArrPlaylist[$tmpIntI]; utf8::decode($tmpStrAlbumTitle); if ($tmpIntI == 1) { $tmpStrAlbumTitle =~ s/^\s+//; } } $tmpStrAlbumTitle =~ s/\.m3u$//; $tmpIntType = 2; $tmpStrTrackNum = "-1"; $tmpStrTitle = ""; if ($optDebug) { my $tmpStrMsg = utf8::is_utf8($tmpStrFile)." | ".utf8::is_utf8($tmpStrFileWOPath)." | ".utf8::is_utf8($tmpStrAlbumArtist)." | ".utf8::is_utf8($tmpStrAlbumTitle); &funcPrintMsg(0, $tmpStrMsg); } } else { &funcPrintMsg(3, "Unknown filetype in $tmpStrFile - don't know what to do."); exit(1); } $tmpStrAlbumArtist = &funcConvStr2FilenameFormat($tmpStrAlbumArtist); $tmpStrAlbumTitle = &funcConvStr2FilenameFormat($tmpStrAlbumTitle); if ($optDebug) { &funcPrintMsg(0,"Filename: $tmpStrFile | Type: $tmpIntType | AlbumArtist: $tmpStrAlbumArtist | AlbumTitle: $tmpStrAlbumTitle | Track: $tmpStrTrackNum | Title: $tmpStrTitle"); } push (@glbArrMP3Info, $glbIntUniqID, $tmpIntType, $tmpStrAlbumArtist, $tmpStrAlbumTitle, $tmpStrTrackNum, $tmpStrTitle, $tmpStrFile); push (@glbArrMP3List, [ @glbArrMP3Info ]); if ($optDebug) { &funcPrintMsg(0,"glbArrMP3Info: ",1); foreach $tmpStrPrint (@glbArrMP3Info) { &funcPrintMsg(-1,"| $tmpStrPrint ",1); } &funcPrintMsg(-1,"|"); } $glbIntUniqID++; } # Sort the array by Type, Album Artist, Album Title and Track Number &funcPrintMsg(1, "Sorting list of MP3s/M3Us found by Type, Album Artist, Album Title and Track Number"); @glbArrMP3ListSorted = sort { $a->[2] cmp $b->[2] || $a->[3] cmp $b->[3] || $a->[1] cmp $b->[1] || $a->[4] cmp $b->[4] } @glbArrMP3List; if ($optDebug) { &funcPrintMsg(0, "Size of sorted array is ".@glbArrMP3ListSorted); foreach $tmpRefList (@glbArrMP3ListSorted) { &funcPrintMsg(0, "@$tmpRefList"); } } # Create a blank second arrary @glbArrToBeLinked; # Loop through each second dimension items in the sorted array and add the Type, Album Artist, Album Title, Track Number and filename info to the second array. $tmpStrPrevAlbumArtist = ""; $tmpStrPrevAlbumTitle = ""; $glbIntI = 0; @glbArrExistingArtists; @glbArrExistingAlbums; foreach $tmpRefList (@glbArrMP3ListSorted) { $#tmpArrFileInfo = -1; &funcLinkFiles(); if ($optDebug) { &funcPrintMsg(0, "Adding to be linked list: @$tmpRefList[1] | @$tmpRefList[2] | @$tmpRefList[3] | @$tmpRefList[4] | @$tmpRefList[6]"); } push (@tmpArrFileInfo, @$tmpRefList[1], @$tmpRefList[2], @$tmpRefList[3], @$tmpRefList[4], @$tmpRefList[6]); push (@glbArrToBeLinked, [ @tmpArrFileInfo ]); $tmpStrPrevAlbumArtist = @$tmpRefList[2]; $tmpStrPrevAlbumTitle = @$tmpRefList[3]; $glbIntI++; } $tmpRefList = $glbArrMP3ListSorted[$tmpIntI - 1]; if ($optDebug) { &funcPrintMsg(0, "Test if we have rolled back to the last item (should be non-blank): @$tmpRefList[2]"); } &funcLinkFiles(); &funcPrintMsg(1, "All done!"); exit(0); # This function USES GLOBAL VARIABLES to work out the grouping of links / directories required # or if the process should skip as we are in the same album still sub funcLinkFiles { if ($optDebug) { &funcPrintMsg(0, "Loop counter: $glbIntI"); } # If the previous second dimenstion item in the array doesn't have a matching Album Artist OR Album Title and is not the first iteration # OR the previous track is the last item in the to be sorted list, if ((((@$tmpRefList[2] ne $tmpStrPrevAlbumArtist) || (@$tmpRefList[3] ne $tmpStrPrevAlbumTitle)) && ($glbIntI > 0)) || ($glbIntI == @glbArrMP3ListSorted)) { &funcPrintMsg(1, "Reached the end of '$tmpStrPrevAlbumArtist | $tmpStrPrevAlbumTitle'. "); # then loop around the second array and check if the album artist and album title directories do not exist - # create them if they do (first element in the array only) my $tmpIntJ = 0; my $tmpStrDirAlbumArtist = ""; my $tmpStrDirAlbumTitle = ""; my $tmpStrLinkTrackNum = ""; my $tmpStrSrcFile = ""; my $tmpStrDestLink = ""; my $tmpIntUID = ""; my $tmpIntGID = ""; my @tmpArrExistingLinks; my $tmpBinPrevCoverArt = ""; my $tmpStrPrevCoverArtType = ""; my $tmpBoolDiffCoverArt = 0; my $tmpBoolNoCoverArt = 0; ($tmpIntUID, $tmpIntGID) = &funcGetUIDGID(); foreach $tmpRefTBLList (@glbArrToBeLinked) { my $tmpBinCoverArt; my $tmpStrCoverArtType = ""; $tmpStrDirAlbumArtist = &funcEncodeUTF8($strLinksDir.@$tmpRefTBLList[1]); $tmpStrDirAlbumTitle = $tmpStrDirAlbumArtist.$strDirSep.&funcEncodeUTF8(@$tmpRefTBLList[2]); $tmpStrSrcFile = @$tmpRefTBLList[4]; $tmpStrSrcFilename = &funcRemovePathFromFilename(@$tmpRefTBLList[4]); if ($optDebug) { print "$tmpStrDirAlbumArtist | $tmpStrDirAlbumTitle | $tmpStrSrcFile | $tmpStrSrcFilename\n"; } if (@$tmpRefTBLList[0] == 1) { my @tmpHashAPIC; my $tmpStrLong = ""; my $tmpObjMP3 = MP3::Tag->new($tmpStrSrcFile); my $tmpObjMP3ID3v2; # Hashing out because the playlists won't work if we include the track number in the soft link # $tmpStrLinkTrackNum = &funcPadTrackNum(@$tmpRefTBLList[3]); # $tmpStrLinkTrackNum .= "-"; $tmpObjMP3->get_tags(); if (exists $tmpObjMP3->{ID3v2}) { $tmpObjMP3ID3v2 = $tmpObjMP3->{ID3v2}; } else { &funcPrintMsg(3, "$tmpStrSrcFile does not have any ID3v2 tags - we've already checked this so shouldn't get here."); exit(1); } (@tmpHashAPIC, $tmpStrLong) = $tmpObjMP3ID3v2->get_frame("APIC"); $tmpObjMP3->close(); foreach $tmpHashAPICData (@tmpHashAPIC) { while (my ($tmpAPICKey, $tmpAPICVal) = each %$tmpHashAPICData) { if ($optDebug) { if ($tmpAPICKey ne "_Data") { &funcPrintMsg(0, "$tmpAPICKey => $tmpAPICVal"); } else { &funcPrintMsg(0, "$tmpAPICKey => JPEG data - won't print this"); } } if ($tmpAPICKey eq "MIME type") { if ( ($tmpAPICVal eq "image/jpeg") || ($tmpAPICVal eq "image/jpg") || ($tmpAPICVal eq "image/gif") || ($tmpAPICVal eq "image/png") ) { $tmpStrCoverArtType = $tmpAPICVal; } else { &funcPrintMsg(3, "We do not support this MIME Type: $tmpAPICVal"); if ($optCreateLinks) { exit(1); } } } elsif ($tmpAPICKey eq "Picture Type") { if ($tmpAPICVal ne "Cover (front)") { &funcPrintMsg(3, "We do not support this Picture Type: $tmpAPICVal"); if ($optCreateLinks) { exit(1); } } } elsif ($tmpAPICKey eq "_Data") { $tmpBinCoverArt = $tmpAPICVal; } } } } elsif (@$tmpRefTBLList[0] == 2) { $tmpStrLinkTrackNum = ""; } else { die "ERROR! Unknown type found in the To Be Linked array during linking:\n@$tmpRefTBLList[0]:@$tmpRefTBLList[1]:@$tmpRefTBLList[2]:@$tmpRefTBLList[3]:@$tmpRefTBLList[4]"; } $tmpStrDestLink = $tmpStrDirAlbumTitle.$strDirSep.$tmpStrLinkTrackNum.$tmpStrSrcFilename; if ($tmpIntJ == 0) { if ( (!-d $tmpStrDirAlbumArtist) && $optCreateLinks ) { &funcCreateDir($tmpStrDirAlbumArtist, $tmpIntUID, $tmpIntGID); } elsif ( (!-d $tmpStrDirAlbumArtist) && !$optCreateLinks) { &funcPrintMsg(4, "We would be creating the Album Artist directory: $tmpStrDirAlbumArtist"); } elsif ($optDebug) { &funcPrintMsg(0, "The directory $tmpStrDirAlbumArtist already exists - leaving it as is."); } if ( (!-d $tmpStrDirAlbumTitle) && $optCreateLinks ) { &funcCreateDir($tmpStrDirAlbumTitle, $tmpIntUID, $tmpIntGID); } elsif ( (!-d $tmpStrDirAlbumTitle) && !$optCreateLinks) { &funcPrintMsg(4, "We would be creating the Album Title directory: $tmpStrDirAlbumTitle"); } elsif ($optDebug) { &funcPrintMsg(0, "The directory $tmpStrDirAlbumTitle already exists - leaving it as is."); } if ($optDelNonExists) { push (@glbArrExistingAlbums, $tmpStrDirAlbumTitle); if ( (grep {$_ ne $tmpStrDirAlbumArtist} @glbArrExistingArtists) || (@glbArrExistingArtists == 0) ) { push (@glbArrExistingArtists, $tmpStrDirAlbumArtist); } } if (@$tmpRefTBLList[0] == 1) { $tmpBinPrevCoverArt = $tmpBinCoverArt; $tmpStrPrevCoverArtType = $tmpStrCoverArtType; if ($tmpBinCoverArt eq "") { if (!$tmpBoolNoCoverArt) { &funcPrintMsg(2, "The cover art for @$tmpRefTBLList[1] - @$tmpRefTBLList[2] is blank."); } if ($optDebug) { &funcPrintMsg(0, "Cover art is blank in $tmpStrSrcFile"); } $tmpBoolNoCoverArt = 1; } } $tmpIntJ++; } else { if (@$tmpRefTBLList[0] == 1) { if ( ($tmpBinPrevCoverArt ne $tmpBinCoverArt) || ($tmpStrPrevCoverArtType ne $tmpStrCoverArtType) ) { my $tmpStrErr = ""; if ($tmpBinPrevCoverArt ne $tmpBinCoverArt) { $tmpStrErr = "Cover different"; } if ($tmpStrPrevCoverArtType ne $tmpStrCoverArtType) { $tmpStrErr .= "|Type different: $tmpStrPrevCoverArtType vs $tmpStrCoverArtType"; } if (!$tmpBoolDiffCoverArt) { &funcPrintMsg(2, "The cover art for @$tmpRefTBLList[1] - @$tmpRefTBLList[2] is different in $tmpStrSrcFile: $tmpStrErr."); } $tmpBoolDiffCoverArt = 1; } elsif ($tmpBinCoverArt eq "") { if (!$tmpBoolNoCoverArt) { &funcPrintMsg(2, "The cover art for @$tmpRefTBLList[1] - @$tmpRefTBLList[2] is blank in $tmpStrSrcFile"); } $tmpBoolNoCoverArt = 1; } } } if ( ( (!-l $tmpStrDestLink) && (!-e $tmpStrDestLink) ) || (-l $tmpStrDestLink) ) { my $tmpBoolErr = 0; if ( (-l $tmpStrDestLink) && $optRecreate ) { if ($optDebug) { &funcPrintMsg(0, "Attempting to delete the link $tmpStrDestLink"); } unlink($tmpStrDestLink) or $tmpBoolErr = 1; } elsif ( ( (-l $tmpStrDestLink) && !$optRecreate ) && $optDebug ) { &funcPrintMsg(0, "The link $tmpStrDestLink already exists - leaving it as is."); } if ($tmpBoolErr) { &funcPrintMsg(3, "Couldn't delete the symlink: $!"); exit(1); } if ( ($optCreateLinks) && (!-l $tmpStrDestLink) ) { if ($optDebug) { &funcPrintMsg(0, "Creating the link $tmpStrDestLink pointing to $tmpStrSrcFile"); } symlink($tmpStrSrcFile, $tmpStrDestLink) or $tmpBoolErr = 1; # Can't do this in PERL - it will chown the actual file ($tmpStrSrcFile) instead of the link # chown ($tmpIntUID, $tmpIntGID, $tmpStrDestLink); } elsif (!-l $tmpStrDestLink) { &funcPrintMsg(4, "We would be creating the symlink $tmpStrDestLink to point to $tmpStrSrcFile"); } if ($tmpBoolErr) { &funcPrintMsg(3, "Couldn't create the symlink: $!"); exit(1); } if ($optDelNonExists) { push (@tmpArrExistingLinks, $tmpStrDestLink); } } elsif (-f $tmpStrDestLink) { &funcPrintMsg(3, "Cannot create soft link - file $tmpStrDestLink already exists."); if (!$optSkipErrs) { exit(1); } } } if (!$tmpBoolNoCoverArt && !$tmpBoolDiffCoverArt) { &funcPrintMsg(1, "Cover art for $tmpStrPrevAlbumArtist - $tmpStrPrevAlbumTitle is OK."); if ($optCreateLinks) { my $tmpStrDirAlbumArtFolder = $tmpStrDirAlbumTitle.$strDirSep."Folder.jpg"; # Not doing AlbumArtLarge - deprecated for WMP11 zzzzzzzzz # Not doing AlbumArtSmall because I can't get ImageMagick installed on the NAS due to the amount of dependencies (X being one) # It isn't really powerful enough to take that, plus don't want to ruin it! # my $tmpStrDirAlbumArtFull = $tmpStrDirAlbumTitle.$strDirSep."AlbumArtLarge.jpg"; # my $tmpStrDirAlbumArtSmall = $tmpStrDirAlbumTitle.$strDirSep."AlbumArtSmall.jpg"; # # open (HNDAPIC, ">$tmpStrDirAlbumArtFull") or $tmpBoolErr = 1; # binmode(HNDAPIC); # print HNDAPIC $tmpBinPrevCoverArt; # close (HNDAPIC); # # chown ($tmpIntUID, $tmpIntGID, $tmpStrDirAlbumArtFull) or $tmpBoolErr = 1; # chmod (oct($glbIntFilePerms), $tmpStrDirAlbumArtFull) or $tmpBoolErr = 1; # # if ($tmpBoolErr) # { # &funcPrintMsg(3, "Couldn't create large album art $tmpStrDirAlbumArtFull: $!"); # exit(1); # } open (HNDAPIC, ">$tmpStrDirAlbumArtFolder") or $tmpBoolErr = 1; binmode(HNDAPIC); print HNDAPIC $tmpBinPrevCoverArt; close (HNDAPIC); chown ($tmpIntUID, $tmpIntGID, $tmpStrDirAlbumArtFolder) or $tmpBoolErr = 1; chmod (oct($glbIntFilePerms), $tmpStrDirAlbumArtFolder) or $tmpBoolErr = 1; if ($tmpBoolErr) { &funcPrintMsg(3, "Couldn't create folder.jpg album art $tmpStrDirAlbumArtFolder: $!"); exit(1); } } else { &funcPrintMsg(4, "We would be creating the Album Art in $tmpStrDirAlbumTitle"); } } if ($optDelNonExists) { &funcRemoveNonExists($tmpStrDirAlbumTitle, @tmpArrExistingLinks); if ( (@$tmpRefList[2] ne $tmpStrPrevAlbumArtist) || ($glbIntI == @glbArrMP3ListSorted) ) { &funcRemoveNonExists($tmpStrDirAlbumArtist, @glbArrExistingAlbums); $#glbArrExistingAlbums = -1; } if ($glbIntI == @glbArrMP3ListSorted) { &funcRemoveNonExists($strLinksDir, @glbArrExistingArtists); } } # Blank the second array at the end of the above IF block. $#glbArrToBeLinked = -1; } } # Function to remove non-existent links / directories - need to pass it an array of items we know are good and the directory to check sub funcRemoveNonExists { my $tmpStrDir = shift; my @tmpArrExisting = @_; my $tmpBoolErr = 0; my $tmpStrLinkCheck = ""; opendir(HNDDIR, $tmpStrDir) or $tmpBoolErr = 1; if ($tmpBoolErr) { &funcPrintMsg(3, "Cannot open $tmpStrDir: $!"); } while ( (defined($tmpStrLinkCheck = readdir(HNDDIR))) && !$tmpBoolErr ) { my $tmpBoolOK = 0; my $tmpStrExistingLink = ""; if ( ($tmpStrLinkCheck =~ m/^\./) || ($tmpStrLinkCheck =~ m/Folder\.jpg/) || ($tmpStrLinkCheck =~ m/AlbumArtLarge\.jpg/) || ($tmpStrLinkCheck =~ m/AlbumArtSmall\.jpg/) ) { next; } if ($tmpStrDir =~ m/$strDirSep$/) { $tmpStrLinkCheck2 = $tmpStrDir.$tmpStrLinkCheck; } else { $tmpStrLinkCheck2 = $tmpStrDir.$strDirSep.$tmpStrLinkCheck; } foreach $tmpStrExistingLink (@tmpArrExisting) { if ($tmpStrExistingLink eq $tmpStrLinkCheck2) { $tmpBoolOK = 1; last; } } if (!$tmpBoolOK) { if ($optDebug) { &funcPrintMsg(0, "Stale link / directory found. Deleting $tmpStrLinkCheck2"); } if (-d $tmpStrLinkCheck2) { &funcRemoveDir($tmpStrLinkCheck2); } else { unlink($tmpStrLinkCheck2) or $tmpBoolErr = 1; } if ($tmpBoolErr) { &funcPrintMsg(3, "Cannot remove $tmpStrLinkCheck2: $!"); last; } } } closedir(HNDDIR); if ($tmpBoolErr) { exit(1); } } # Function to return the just the filename from a filename containing the path sub funcRemovePathFromFilename { my $tmpStrFile = shift; my $tmpIntPos = rindex($tmpStrFile, $strDirSep); my $tmpStrFileWOPath = substr($tmpStrFile, ($tmpIntPos + 1)); return $tmpStrFileWOPath; } # Function to remove characters that Windows cannot deal with in directories / files (pansy) sub funcConvStr2FilenameFormat { my $tmpStr2Conv = shift; $tmpStr2Conv =~ s/\*/x/g; $tmpStr2Conv =~ s/\\/-/g; $tmpStr2Conv =~ s/\//-/g; $tmpStr2Conv =~ s/:/-/g; $tmpStr2Conv =~ s/"/''/g; $tmpStr2Conv =~ s//]/g; $tmpStr2Conv =~ s/\?//g; $tmpStr2Conv =~ s/\|/_/g; $tmpStr2Conv =~ s/\.+$//; $tmpStr2Conv = &funcDecodeUTF8($tmpStr2Conv); return $tmpStr2Conv; } # Function to encode any UTF8 strings so we don't get double character weirdness # in the directory / filenames in the links directory sub funcEncodeUTF8 { my $tmpStrToEncode = shift; if ( !utf8::is_utf8($tmpStrToEncode) ) { if ($optDebug) { &funcPrintMsg(0, "$tmpStrToEncode is NOT utf8 - encoding."); } utf8::encode($tmpStrToEncode); } else { if ($optDebug) { &funcPrintMsg(0, "$tmpStrToEncode is utf8 - skipping."); } } return $tmpStrToEncode; } # Function to decode any UTF8 strings so we don't get double character weirdness # in the directory / filenames in the links directory sub funcDecodeUTF8 { my $tmpStrToDecode = shift; # if ( utf8::is_utf8($tmpStrToDecode) ) # { # if ($optDebug) { &funcPrintMsg(0, "$tmpStrToDecode IS utf8 - decoding."); } if ($optDebug) { &funcPrintMsg(0, "$tmpStrToDecode being utf8 decoded regardless of what we think it might encoded as."); } utf8::decode($tmpStrToDecode); # } # else # { # if ($optDebug) { &funcPrintMsg(0, "$tmpStrToDecode is NOT utf8 - skipping."); } # } return $tmpStrToDecode; } # Function to recursively delete a directory and it's contents sub funcRemoveDir { my $tmpStrDir = shift; my $tmpBoolErr = 0; my $tmpStrDirList = ""; local *HNDDIR2; opendir(HNDDIR2,$tmpStrDir) or $tmpBoolErr = 1; while ( (defined($tmpStrDirList = readdir(HNDDIR2))) && !$tmpBoolErr ) { if ($tmpStrDirList =~ m/^\./) { next; } my $tmpStr2Unlink = $tmpStrDir.$strDirSep.$tmpStrDirList; if ( (-f $tmpStr2Unlink) || (-l $tmpStr2Unlink) ) { unlink($tmpStr2Unlink) or $tmpBoolErr = 1; } elsif (-d $tmpStr2Unlink) { &funcRemoveDir($tmpStr2Unlink); } if ($tmpBoolErr) { &funcPrintMsg(3, "Cannot remove $tmpStr2Unlink: $!"); exit(1); } } closedir(HNDDIR2); rmdir($tmpStrDir) or $tmpBoolErr = 1; if ($tmpBoolErr) { &funcPrintMsg(3, "Cannot remove directory $tmpStr2Unlink: $!"); exit(1); } } # Function to get the UID and GID of the user Music and group grpMusic_RW sub funcGetUIDGID { my $tmpIntUID; my $tmpIntGID; my $tmpStrUName = "Music"; my $tmpStrGName = "grpMusic_RW"; my $tmpBoolErr = 0; $tmpIntUID = getpwnam($tmpStrUName) or $tmpBoolErr = 1; $tmpIntGID = getgrnam($tmpStrGName) or $tmpBoolErr = 1; if (!$tmpIntUID) { &funcPrintMsg(3, "Cannot find the UID for $tmpStrUName."); } if (!$tmpIntGID) { &funcPrintMsg(3, "Cannot find the GID for $tmpStrGName."); } if (!$tmpIntUID || !$tmpIntGID || $tmpBoolErr) { exit(1); } return $tmpIntUID, $tmpIntGID; } # Takes in a string, UID and GID; makes the directory and permissions it with $glbStrDirPerms; changes ownership to $tmpIntUID:$tmpIntGID sub funcCreateDir { my $tmpStrDir = shift; my $tmpIntUID = shift; my $tmpIntGID = shift; my $tmpBoolErr = 0; $tmpStrDir =~ s/\*/x/g; mkdir ($tmpStrDir) or $tmpBoolErr = 1; if ($tmpBoolErr) { &funcPrintMsg(3, "Cannot create directory $tmpStrDir: $!"); } else { chmod (oct($glbIntDirPerms), $tmpStrDir) or $tmpBoolErr = 1; if ($tmpBoolErr) { &funcPrintMsg(3, "Cannot change permissions on $tmpStrDir to ".oct($glbIntDirPerms).": $!"); } else { chown ($tmpIntUID, $tmpIntGID, $tmpStrDir) or $tmpBoolErr = 1; if ($tmpBoolErr) { &funcPrintMsg(3, "Cannot change ownership on $tmpStrDir to $tmpIntUID:$tmpIntGID - $!"); } } } if ($tmpBoolErr && !$optSkipErrs) { exit(1); } } # Pads the track number based on the number of total tracks. If not present, we pad as 2 digits. sub funcPadTrackNum { my @tmpArrTrackNum = split(/\//,shift); my $tmpStrTrackNum = $tmpArrTrackNum[0]; if (@tmpArrTrackNum > 1) { $tmpStrTrackTotal = $tmpArrTrackNum[1]; } if ( !$tmpArrTrackNum[1] || (length($tmpStrTrackNum) == 1) ) { $tmpIntPadding = 2; } else { $tmpIntPadding = length($tmpArrTrackNum[1]); } my $tmpStrSPrintF = "%0".$tmpIntPadding."d"; $tmpStrTrackNum = sprintf($tmpStrSPrintF, $tmpStrTrackNum); return $tmpStrTrackNum; } sub funcSearch4MP3s { foreach my $tmpStrMP3Dir (@glbArrMP3Dir) { my $tmpStrFile = ""; &funcReadDir($tmpStrMP3Dir); } &funcPrintMsg(1, "Found $glbIntMP3Cnt MP3s and $glbIntM3UCnt playlists"); if ($glbIntMP3Cnt == 0) { &funcPrintMsg(2,"There were no MP3 files found. Quitting"); exit(0); } } sub funcReadDir { my $tmpStrMP3Dir = shift; my @tmpArrDirList; &funcPrintMsg(1, "Reading list of MP3 and M3U files from $tmpStrMP3Dir"); foreach my $tmpStrExcludeDir (@glbArrExcludeDirs) { if ($tmpStrMP3Dir =~ m/^$tmpStrExcludeDir/) { &funcPrintMsg(1, "Skipping directory $tmpStrMP3Dir because it's on the exclude list."); return; } } opendir(HNDDIR, $tmpStrMP3Dir) or die "ERROR! Can't open $tmpStrMP3Dir: $!"; my $tmpStrFile = ""; while( defined ($tmpStrFile = readdir HNDDIR) ) { my $tmpStrFile2 = $tmpStrMP3Dir.$tmpStrFile; if ($tmpStrFile !~ /^\./) { push (@tmpArrDirList, $tmpStrFile2); } } closedir(HNDDIR); foreach my $tmpStrDirList (@tmpArrDirList) { if ( (-d $tmpStrDirList) && $optRecursive ) { &funcReadDir($tmpStrDirList.$strDirSep); } elsif ( ($tmpStrDirList =~ /\.mp3$/) || ($tmpStrDirList =~ /\.m3u$/) ) { if ($optDebug) { &funcPrintMsg(0,"Found $tmpStrFile"); } push (@glbArrDirList, $tmpStrDirList); } if ( $tmpStrDirList =~ /\.m3u$/ ) { $glbIntM3UCnt++; } elsif ( $tmpStrDirList =~ /\.mp3$/ ) { $glbIntMP3Cnt++; } } } # Displays help on the console sub funcDisplayHelp { print "Usage:\n"; print "======\n\n"; print " $0 [--debug] --mp3dir= --linksdir= [--createlinks] [--delnonexists] [--skiperrs]|--help\n\n"; print " --createlinks ** PLEASE DO NOT USE THIS ON THE FIRST RUN - without **\n"; print " ** it, we will show you tracks that don't have ID3v2 **\n"; print " ** tags or don't have the AlbumArtist tag populated. **\n"; print " ** Creates folders / links in --linksdir from MP3s **\n"; print " ** found in --mp3dir. Any action that would have **\n"; print " ** occurred is displayed on STDOUT prefixed by **\n"; print " ** *ACTION*. This information is automatically **\n"; print " ** included if you run with the --debug option. **\n\n"; print " --debug Displays debug processing information\n\n"; print " --delnonexists If tracks have been removed / renamed, then links in the\n"; print " folder structure that point to MP3s that are not present\n"; print " in --mp3dir, are removed.\n"; print " --excludemp3dir If you have used the --recursive option and have dirs\n"; print " within the location specified in --mp3dir, you can add\n"; print " directories under it to an exclude list where we will\n"; print " not process any MP3s found.\n\n"; print " --help Displays this help information\n\n"; print " --linksdir= Output folders for artists and albums including soft\n"; print " links to the actual tracks will be written here.\n\n"; print " --mp3dir= Script will search for MP3s to create the links\n"; print " to represent a flat MP3 collection in a folder structure\n"; print " that Media Center et al. can understand. To specify\n"; print " multiple directories, separate them with a : (colon)\n"; print " The Location where the links / folders are written to\n"; print " is set by --linksdir option.\n"; print " ** THIS MUST BE A FULL RELATIVE PATH **\n\n"; print " --recreate Can only be used with --createlinks. This will remove\n"; print " any existing links and recreate them.\n\n"; print " --recursive Use this option if you want us to recursively look for\n"; print " MP3s within the directories specified in --mp3dir\n\n"; print " --skiperrs If there are any errors with creating directories /\n"; print " links, then do not stop processing.\n"; } sub funcPrintMsg { ($tmpIntType, $tmpStrMessage, $tmpBoolNoNewline) = ($_[0], $_[1], $_[2]); if ($tmpIntType >= 0) { if ($tmpIntType == 0) { $tmpStrType = "DEBUG"; } elsif ($tmpIntType == 1) { $tmpStrType = "INFO"; } elsif ($tmpIntType == 2) { $tmpStrType = "WARNING"; } elsif ($tmpIntType == 3) { $tmpStrType = "ERROR!"; } elsif ($tmpIntType == 4) { $tmpStrType = "ACTION"; } else { $tmpStrType = "UNKNOWN"; } ($tmpIntSec,$tmpIntMin,$tmpIntHour,$tmpIntMDay,$tmpIntMon,$tmpIntYear,$tmpIntWDay,$tmpIntYDay,$tmpBoolDst)=localtime(time); printf "%4d-%02d-%02d %02d:%02d:%02d",$tmpIntYear+1900,$tmpIntMon+1,$tmpIntMDay,$tmpIntHour,$tmpIntMin,$tmpIntSec; print " - "; printf "%-7s", $tmpStrType; print " - "; } print "$tmpStrMessage"; if (!$tmpBoolNoNewline) { print "\n"; } }