//package org.serviio.library.online import java.text.* import java.util.regex.* import org.jaudiotagger.audio.* import org.jaudiotagger.tag.* import org.serviio.config.* import org.serviio.library.metadata.* import org.serviio.library.online.* import org.serviio.util.* import org.slf4j.* import org.xml.sax.* import com.sun.org.apache.xerces.internal.impl.io.* /** *

Serviio 'playlist' extractor plugin for formats: m3u, extm3u, pls, asx, smil, itunes, xspf


* Some generell remarks on using the plugin: *
* Principle construction of Serviio console 'Online sources' - 'Source URL' entries: * *
* Principle construction of each switch: * *
* Meaning and use of the different switches: * *
* Noticed extensions of the playlists: *
  1. .m3u
  2. *
  3. .xml (these files will be tested whether the playlist plugin is able to extract the files, i.e. the file has a known playlist format)
  4. *
  5. .asx
  6. *
  7. .wpl
  8. *
  9. .pls
  10. *
  11. .xspf
  12. *
  13. .smi
  14. *
  15. .smil
  16. *
* Possible formats for the media paths inside a playlist: *
  1. valid url including scheme and scheme specific part (http://..., mms://... etc.)
  2. *
  3. [file:]{letter local drive}:{path to playlist}.{extension} (Windows only; '\' or '/': both work)
  4. *
  5. [file:]//{name server}|localhost/{share name of drive}{path to playlist}.{extension} (Windows only; '\' or '/': both work)
  6. *
  7. [file:]{path to playlist from scratch}.{extension} (Linux only; '\' or '/': both work)
  8. *
  9. relative pathes starting from playlist path
  10. *
* Tested for
* Windows 7 and QNAP Linux *
* * @author Olaf Ahrens * @see playlist information by Lucas Gonze * @see reference xspf playlist format * @see reference asx elements * @see reference smil playlist format * @see org.jaudiotagger by Paul Taylor * @version 3.2 */ class Playlist extends WebResourceUrlExtractor { //* * * constants * * * //* * * * * * * * * * * //* * * system * * * private final VALID_WINDOWS_DRIVE_LETTER = '[A-Za-z]:' private static final String TEMP_FOLDER private static final String VALID_OS_NAME_WINDOWS = '(?i)windows.*' private static final String VALID_OS_NAME_MAC = '(?i)mac.*' //* * * cover art * * * private static final String COVER_ART_LIST_NAME = "PlaylistPluginCovers.txt" private static final String COVER_ART_LIST_PATH_NAME private static final String VALID_COVER_ART_FILE_BEGIN = "PlaylistPluginCoverArt" private static final String VALID_COVER_ART_FILE = VALID_COVER_ART_FILE_BEGIN + "-?\\d+\\.(jpg|png)" private static final Integer ESTIMATED_MAX_NUMBER_COVER_ART_HEAD_BYTES = 30 private static final File coverArtList private static final Map coverArtListMap = new HashMap(100) //* * * playlists * * * private final String VALID_PLAYLIST_URL = '(?i).*\\.(m3u|xml|asx|wpl|pls|xspf|smi|smil)' private final String VALID_PLAYLIST_XML = '(?i).*\\.xml' private final String LINK_TOKEN_SEPARATOR = '-SW-' private static final Integer ESTIMATED_MEDIA_COUNT = 50 //* * * playlist extraction * * * private final String DEFAULT_PLAYLIST_NAME_PATTERN = '-ar - -tt' private final String[] NO_VALID_PLAYLIST_LINES_M3U = [ '#.*', '\\\'.*', '(?i)rem .*' ] private final String[] NO_VALID_PLAYLIST_LINES_EXTM3U = [ '\\\'.*', '(?i)rem .*', '(?i)#extm3u.*' ] private final String[] NO_VALID_PLAYLIST_LINES_PLS = [ '#.*', '\\\'.*', '(?i)rem.* ', '(?i)\\[playlist\\].*', '(?i)numberof.*', '(?i)version=\\d.*' ] private final String VALID_TRACK_NUMBER_PLS = '\\d+' private final String VALID_LINE_HEAD_BEGIN_PLS = '[A-Za-z]{3,10}' private final String VALID_LINE_HEAD_END_PLS = '=.*' private final String VALID_LINE_HEAD_COMPLETE_PLS = VALID_LINE_HEAD_BEGIN_PLS + VALID_TRACK_NUMBER_PLS + VALID_LINE_HEAD_END_PLS private final String VALID_LINE_FILE_BEGIN_PLS = '(?i)file' private final String VALID_LINE_FILE_COMPLETE_PLS = VALID_LINE_FILE_BEGIN_PLS + VALID_TRACK_NUMBER_PLS + VALID_LINE_HEAD_END_PLS private final String VALID_LINE_TITLE_BEGIN_PLS = '(?i)title' private final String[] VALID_MEDIA_ELEMENTS_ASX = ['entry', 'Entry', 'ENTRY'] private final String[] VALID_MEDIA_ARTIST_ELEMENTS_ASX = [ 'author', 'Author', 'AUTHOR' ] private final String[] VALID_MEDIA_TITLE_ELEMENTS_ASX = ['title', 'Title', 'TITLE'] private final String[] VALID_MEDIA_URL_ELEMENTS_ASX = ['ref', 'Ref', 'REF'] private final String[] VALID_MEDIA_URL_ATTRIBUTES_ASX = [ '@href', '@Href', '@HRef', '@HREF' ] private final String[] VALID_MEDIA_URL_ELEMENTS_SMIL = [ 'ref', 'audio', 'video', 'media' ] private final Integer ESTIMATED_COUNT_PLAYLISTS_ITUNES = 5 private final Integer ESTIMATED_XML_KEYS_COUNT_ITUNES = 10 private final String VALID_ALL_TRACKS_ITUNES = '(?i)all' private final String VALID_EXISTING_NUMBERING = '^[\\d*[\\.*-*_*\\s*]*]*\\s*-*\\s*' //* * * media * * * private static final String VALID_MEDIA_WEB_URL = '(?i)^(https?|rtmp[ts]?|mms|rtsp|rtp|rtcp|sip|ftp)://(.*\\.[A-Za-z]{2,6}|@?[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(:[0-9]{0,5})?(/.*)?$' //private static final String VALID_MEDIA_WIN_URL = '(?i)(file:)?(//[A-Za-z0-9_-\\(\\)@#�%\\.!]+/[A-Za-z0-9_-\\(\\)@#�%\\.!]+/|[A-Za-z]:/).*\\.[A-Za-z0-9]{2,4}' //private static final String VALID_MEDIA_UNIX_URL = '(?i)(file:/)?(/)*[A-Za-z0-9_-\\(\\)@#�%\\.!]+/.*\\.[A-Za-z0-9]{2,4}' private final String[] VIDEO_EXTENSIONS = [ '.mp4', '.flv', '.f4v', '.m4v', '.mp4v', '.avi', '.mpeg', '.mkv', '.3pg', '.wmv', '.wmp', '.wm', '.asf', '.divx', '.mov', '.ogm', '.ogv' ] private final VALID_MEDIA_FILE_PROTOCOL = '(?i)file:.*' //* * * public methods * * * //* * * * * * * * * * * * * * @Override public String getExtractorName() { return 'Playlist extractor' } @Override public boolean extractorMatches(URL feedUrl) { String playlistUrl = new LinkTokenizer(feedUrl).getPlaylistUrl() if (playlistUrl ==~ VALID_PLAYLIST_XML) { return !(new PlaylistIdentifier(playlistUrl).getPlaylistType() in [ PlaylistTypes.ERROR_XML_UNKNOWN, PlaylistTypes.ERROR_XML_WRONG_ENCODED, PlaylistTypes.ERROR_XML_PARSER, PlaylistTypes.ERROR_XML_IO ]) } else { return playlistUrl ==~ VALID_PLAYLIST_URL } } /** * Extracts a given playlist named by its URL
*
* Possible playlist formats: m3u, extm3u, pls, asx, smil, itunes, xspf */ @Override protected WebResourceContainer extractItems(URL resourceUrl, int maxItems) { logg(' - OS: ' + ThisPlatform.getPlatform().toString()) //* * * decoding resourceUrl: declaring and naming playlistUrl, playlistName, playlistTumbnailUrl, live status, itunesPlaylistNames LinkTokenizer myLinkTokenizer = new LinkTokenizer(resourceUrl) String playlistUrl = myLinkTokenizer.getPlaylistUrl() String playlistThumbnailUrl = myLinkTokenizer.getPlaylistThumbnailUrl() String playlistName = myLinkTokenizer.getPlaylistName() String playlistNamePattern = myLinkTokenizer.getPlaylistNamePattern() List itunesPlaylistNames = myLinkTokenizer.getItunesPlaylistNames() PlaylistLiveStatus playlistLive = myLinkTokenizer.getPlaylistLive() MediaFileType playlistMediaType = myLinkTokenizer.getPlaylistMediaType() Boolean playlistNumbering = myLinkTokenizer.getPlaylistNumbering() //* * * identifying type of playlist PlaylistIdentifier myPlaylistIdentifier = new PlaylistIdentifier(playlistUrl) PlaylistTypes playlistType = myPlaylistIdentifier.getPlaylistType() String[] playlistLines = myPlaylistIdentifier.getPlaylistLines() logg(' - playlistType: ' + playlistType.toString()) //* * * read cover art list readCoverArtList() //* * * handle different types of playlists AbstractPlaylistExtractor myPlaylistExtractor if (playlistType == PlaylistTypes.M3U) { myPlaylistExtractor = new PlaylistExtractorM3u(playlistType, playlistUrl, playlistLines, playlistThumbnailUrl, playlistName, playlistNamePattern, itunesPlaylistNames, playlistLive, playlistMediaType) } else if (playlistType == PlaylistTypes.EXTM3U) { myPlaylistExtractor = new PlaylistExtractorExtm3u(playlistType, playlistUrl, playlistLines, playlistThumbnailUrl, playlistName, playlistNamePattern, itunesPlaylistNames, playlistLive, playlistMediaType) } else if (playlistType == PlaylistTypes.PLS) { myPlaylistExtractor = new PlaylistExtractorPls(playlistType, playlistUrl, playlistLines, playlistThumbnailUrl, playlistName, playlistNamePattern, itunesPlaylistNames, playlistLive, playlistMediaType) } else if (playlistType == PlaylistTypes.ITUNES) { myPlaylistExtractor = new PlaylistExtractorItunes(playlistType, playlistUrl, playlistLines, playlistThumbnailUrl, playlistName, playlistNamePattern, itunesPlaylistNames, playlistLive, playlistMediaType) } else if (playlistType == PlaylistTypes.ASX) { myPlaylistExtractor = new PlaylistExtractorAsx(playlistType, playlistUrl, playlistLines, playlistThumbnailUrl, playlistName, playlistNamePattern, itunesPlaylistNames, playlistLive, playlistMediaType) } else if (playlistType == PlaylistTypes.SMIL) { myPlaylistExtractor = new PlaylistExtractorSmil(playlistType, playlistUrl, playlistLines, playlistThumbnailUrl, playlistName, playlistNamePattern, itunesPlaylistNames, playlistLive, playlistMediaType) } else if (playlistType == PlaylistTypes.XSPF) { myPlaylistExtractor = new PlaylistExtractorXspf(playlistType, playlistUrl, playlistLines, playlistThumbnailUrl, playlistName, playlistNamePattern, itunesPlaylistNames, playlistLive, playlistMediaType) } else { logg('--- playlist refused: ' + playlistUrl) return null } //* * * write cover art list writeCoverArtList() return new WebResourceContainer(title: playlistName, items: myPlaylistExtractor.getMediaItemsAll(playlistNumbering) as List) } /** * To transfer extracted mediaUrls and information according to the media to Serviio * @param mediaItem: in PlaylistExtractorXXX assembled mediaItems * @param quality: no impact */ @Override protected ContentURLContainer extractUrl(WebResourceItem mediaItem, PreferredQuality quality) { //* * * extracting mediaItem to playlistThumbnailUrl, mediaUrl, mediaType, mediaLive; ignoring quality String mediaThumbnailUrl = mediaItem.getAdditionalInfo()[InfoItems.MEDIATHUMBNAILURL.getText()] String mediaUrl = mediaItem.getAdditionalInfo()[InfoItems.MEDIAURL.getText()] MediaFileType mediaType = mediaItem.getAdditionalInfo()[InfoItems.MEDIATYPE.getText()] if (mediaType == null) { mediaType = (mediaUrl.substring(mediaUrl.lastIndexOf('.')).toLowerCase() in VIDEO_EXTENSIONS)? (MediaFileType.VIDEO) : (MediaFileType.AUDIO) } logg(' - mediaType: ' + mediaType.toString()) Boolean mediaLive = mediaItem.getAdditionalInfo()[InfoItems.MEDIALIVE.getText()] return new ContentURLContainer(fileType: mediaType, contentUrl: mediaUrl, thumbnailUrl: mediaThumbnailUrl, live: mediaLive) } /** * for testing */ static void main(String[] args) { String testUrl = 'http://-SW-uhttp://httpmedia.radiobremen.de/bremeneins.m3u' Playlist pl = new Playlist() assert pl.extractorMatches(new URL(testUrl)) WebResourceContainer container = pl.extractItems(new URL('http://-SW-uhttp://httpmedia.radiobremen.de/bremeneins.m3u'), 1) WebResourceItem item = new WebResourceItem(title: 'Test', additionalInfo: ['mediaUrl': 'http://xyz.mp4', 'm3uThumbnailUrl':'http://xyz.jpg', 'mediaLive':'true']) ContentURLContainer result = pl.extractUrl(item, PreferredQuality.MEDIUM) } //* * * private classes * * * //* * * * * * * * * * * * * * /** * Identifies type of playlist and splits it into a Array of lines */ private class PlaylistIdentifier { private static PlaylistTypes playlistType private static String[] playlistLines protected PlaylistIdentifier(String playlistUrl) { groovy.util.Node playlistXmlNode playlistLines = new BufferedReader(new UnicodeReader(new URL(playlistUrl).openConnection().getInputStream(), null)).readLines().toArray() assert (!Playlist.isNullOrEmpty(playlistLines)) if (playlistLines[0].toLowerCase().startsWith(PlaylistTypes.EXTM3U.getSignature())) { playlistType = PlaylistTypes.EXTM3U } else if (playlistLines[0].toLowerCase().startsWith(PlaylistTypes.PLS.getSignature())) { playlistType = PlaylistTypes.PLS } else { try { playlistXmlNode = new PlaylistXmlParser(false, false).parse(playlistUrl) Playlist.logg(' - XML name: ' + playlistXmlNode.name().toString()) if (playlistXmlNode.name().toString().toLowerCase() == PlaylistTypes.ITUNES.getSignature()) { playlistType = PlaylistTypes.ITUNES } else if (playlistXmlNode.name().toString().toLowerCase() == PlaylistTypes.ASX.getSignature()) { playlistType = PlaylistTypes.ASX } else if (playlistXmlNode.name().toString().toLowerCase() == PlaylistTypes.SMIL.getSignature()) { playlistType = PlaylistTypes.SMIL } else if (playlistXmlNode.name().toString().toLowerCase() == PlaylistTypes.XSPF.getSignature()) { playlistType = PlaylistTypes.XSPF } } catch(MalformedByteSequenceException e) { playlistType = PlaylistTypes.ERROR_XML_WRONG_ENCODED } catch(SAXParseException e) { if (!(playlistLines[0].startsWith('<'))) { playlistType = PlaylistTypes.M3U } else { playlistType = PlaylistTypes.ERROR_XML_UNKNOWN } } catch(SAXException e) { playlistType = PlaylistTypes.ERROR_XML_PARSER } catch(IOException e) { playlistType = PlaylistTypes.ERROR_XML_IO } } } protected PlaylistTypes getPlaylistType() {return playlistType} protected String[] getPlaylistLines() {return playlistLines} } /** * Splits from Serviio transmitted URL by separators '-SW-' and '-SWITCH-' and provides transmitted informations */ private class LinkTokenizer { private String playlistUrl private String playlistThumbnailUrl private String playlistName private String playlistNamePattern private List itunesPlaylistNames = new ArrayList(ESTIMATED_COUNT_PLAYLISTS_ITUNES) private PlaylistLiveStatus playlistLive = PlaylistLiveStatus.UNKNOWN private String playlistMediaTypeRaw private Boolean playlistNumbering = true private String[] linkTokens protected LinkTokenizer(URL resourceUrl) { linkTokens = resourceUrl.toString().replaceAll('-SWITCH-', '-SW-').split(LINK_TOKEN_SEPARATOR) for (Integer i in 1..<(linkTokens.length)) { if (linkTokens[i].startsWith(PlaylistUrlProperties.URL.getAbbreviation())) { //u = playlistUrl playlistUrl = linkTokens[i].substring(1) } else if (linkTokens[i].startsWith(PlaylistUrlProperties.PICTURE.getAbbreviation())) { //p = playlistThumbnailUrl playlistThumbnailUrl = linkTokens[i].substring(1) } else if (linkTokens[i].startsWith(PlaylistUrlProperties.NAME.getAbbreviation())) { //n = playlistName playlistName = URLDecoder.decode(linkTokens[i].substring(1),'UTF-8') } else if (linkTokens[i].startsWith(PlaylistUrlProperties.NAMEPATTERN.getAbbreviation())) { //a = playlistNamePattern playlistNamePattern = URLDecoder.decode(linkTokens[i].substring(1),'UTF-8') } else if (linkTokens[i].startsWith(PlaylistUrlProperties.ITUNES_PL_NAME.getAbbreviation())) { //i = itunesPlaylistName itunesPlaylistNames << URLDecoder.decode(linkTokens[i].substring(1),'UTF-8') } else if (linkTokens[i].startsWith(PlaylistUrlProperties.LIVE_STATUS.getAbbreviation())) { //l = playlistLive playlistLive = PlaylistLiveStatus.getStatus(linkTokens[i].substring(1)) } else if (linkTokens[i].startsWith(PlaylistUrlProperties.TYPE.getAbbreviation())) { //t = playlistMediaType playlistMediaTypeRaw = linkTokens[i].substring(1).toUpperCase() } else if (linkTokens[i].startsWith(PlaylistUrlProperties.NUMBERING.getAbbreviation())) { //m = no playlistNumbering playlistNumbering = false } } } protected String getPlaylistUrl() { Playlist.logg(' - transmitted from WebResourceParser - playlistUrl: ' + playlistUrl) return playlistUrl } protected String getPlaylistThumbnailUrl() { Playlist.logg(' - transmitted from WebResourceParser - playlistThumbnailUrl: ' + playlistThumbnailUrl) return playlistThumbnailUrl } protected String getPlaylistName() { Playlist.logg(' - transmitted from WebResourceParser - playlistName: ' + playlistName) return playlistName } protected String getPlaylistNamePattern() { Playlist.logg(' - transmitted from WebResourceParser - playlistNamePattern: ' + playlistNamePattern) if (!Playlist.isNullOrEmpty(playlistNamePattern)) { for (PlaylistNameTags myPlaylistNameTag in PlaylistNameTags.values()) { if (playlistNamePattern.contains(myPlaylistNameTag.getAbbreviation())) { return playlistNamePattern.trim() } } } Playlist.logg(' - transmitted playlistNamePattern replaced by: ' + DEFAULT_PLAYLIST_NAME_PATTERN) return DEFAULT_PLAYLIST_NAME_PATTERN } protected ArrayList getItunesPlaylistNames() { Playlist.logg(' - transmitted from WebResourceParser - itunesPlaylistName: ' + itunesPlaylistNames.toString()) if (itunesPlaylistNames.any({it ==~ VALID_ALL_TRACKS_ITUNES})) { Playlist.logg(' - transmitted itunesPlaylistName replaced by: []') return [] } return itunesPlaylistNames } protected PlaylistLiveStatus getPlaylistLive() { Playlist.logg(' - transmitted from WebResourceParser - playlistLive: ' + playlistLive.toString()) return playlistLive } protected MediaFileType getPlaylistMediaType() { Playlist.logg(' - transmitted from WebResourceParser - playlistMediaType: ' + playlistMediaTypeRaw) try { return MediaFileType.valueOf(playlistMediaTypeRaw) } catch(e) { //unknown enum item: playlistMediaType stays null Playlist.logg(' - transmitted playlistMediaType replaced by: null') return null } } protected Boolean getPlaylistNumbering() { Playlist.logg(' - transmitted from WebResourceParser - playlistNumbering: ' + playlistNumbering.toString()) return playlistNumbering } } /** * Abstract base class for specific PlaylistExtractorXXX classes */ private abstract class AbstractPlaylistExtractor { protected List mediaItems = new ArrayList(Playlist.ESTIMATED_MEDIA_COUNT) protected ConstructMedium myMedium = new ConstructMedium() protected ArrayList getMediaItemsAll(Boolean playlistNumbering) { if (!Playlist.isNullOrEmpty(mediaItems)) { //numbering of tracks and logging each mediaItem DecimalFormat myFormat = new DecimalFormat(('0' * ((Math.ceil(Math.log10((mediaItems.size() + 1) as Double))) as Integer)) as String) mediaItems.eachWithIndex {WebResourceItem v, Integer i -> //numbering of tracks if (playlistNumbering) { if (v.getTitle() ==~ VALID_EXISTING_NUMBERING) { v.setTitle(myFormat.format(i + 1).toString() + ' - ' + v.getTitle()) } else { v.setTitle(myFormat.format(i + 1).toString() + ' - ' + v.getTitle().replaceFirst(VALID_EXISTING_NUMBERING, '')) } } //logging Playlist.logg(' - transmitted to ContentURLContainer extractUrl - ' + InfoItems.MEDIATHUMBNAILURL.getText() + ': ' + v.getAdditionalInfo()[InfoItems.MEDIATHUMBNAILURL.getText()]) Playlist.logg(' - transmitted to ContentURLContainer extractUrl - ' + InfoItems.MEDIATYPE.getText() + ': ' + v.getAdditionalInfo()[InfoItems.MEDIATYPE.getText()]) Playlist.logg(' - transmitted to ContentURLContainer extractUrl - ' + InfoItems.MEDIAURL.getText() + ': ' + v.getAdditionalInfo()[InfoItems.MEDIAURL.getText()]) Playlist.logg(' - transmitted to ContentURLContainer extractUrl - mediaName: ' + v.getTitle()) Playlist.logg(' - transmitted to ContentURLContainer extractUrl - ' + InfoItems.MEDIALIVE.getText() + ': ' + v.getAdditionalInfo()[InfoItems.MEDIALIVE.getText()]) Playlist.logg('--- medium cached in mediaItems') } } return mediaItems } } private class PlaylistExtractorM3u extends AbstractPlaylistExtractor { protected PlaylistExtractorM3u(PlaylistTypes playlistType, String playlistUrl, String[] playlistLines, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, List itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType) for (Integer i in 0.. itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { String mediaName myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType) for (Integer i in 0.. itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType) for (Integer i = 0; i < playlistLines.length; i++) { //'old' syntax, because of changing i inside the loop Playlist.logg(' - playlistLine no: ' + i.toString() + ', content: ' + playlistLines[i]) //skip empty lines, comments and PLS technical lines if (!(NO_VALID_PLAYLIST_LINES_PLS.any{playlistLines[i].trim() ==~ it} || playlistLines[i].trim().size() == 0)) { if (playlistLines[i] ==~ VALID_LINE_HEAD_COMPLETE_PLS) { //specifying a bucket of playlistLines that belong to one track String trackNumber = (playlistLines[i] =~ VALID_TRACK_NUMBER_PLS)[0] Integer bucketStartLine = i Integer bucketEndLine = i + 1 while (!(bucketEndLine >= playlistLines.length || ((playlistLines[bucketEndLine] ==~ VALID_LINE_HEAD_COMPLETE_PLS) && (!(playlistLines[bucketEndLine] ==~ VALID_LINE_HEAD_BEGIN_PLS + trackNumber + VALID_LINE_HEAD_END_PLS))))) { bucketEndLine++ } bucketEndLine-- i = bucketEndLine //looking for playlistUrl inside the bucket Integer mediaUrlLine foundMediaUrl: for (Integer j in bucketStartLine..bucketEndLine) { if (playlistLines[j] ==~ VALID_LINE_FILE_BEGIN_PLS + trackNumber + VALID_LINE_HEAD_END_PLS) { mediaUrlLine = j break foundMediaUrl } } //specifying medium if (myMedium.setMediaUrl(playlistLines[mediaUrlLine].trim())) { myMedium.setMediaName(findMediaNamePls(playlistLines, bucketStartLine, bucketEndLine, trackNumber)) mediaItems << myMedium.getMediaItem() myMedium.reset() } else { Playlist.logg('--- medium refused, seems not to be a valid URL') } } } } } /** * Reads PLS media name (if contained) from a bucket of associated playlist entries for one track * @param playlistLines * @param bucketStartLine * @param bucketEndLine * @param trackNumber * @return mediaName */ private String findMediaNamePls(String[] playlistLines, Integer bucketStartLine, Integer bucketEndLine, String trackNumber) { for (Integer j in bucketStartLine..bucketEndLine) { if (playlistLines[j] ==~ VALID_LINE_TITLE_BEGIN_PLS + trackNumber + VALID_LINE_HEAD_END_PLS) { return playlistLines[j].substring(playlistLines[j].indexOf('=') + 1) } } } } private class PlaylistExtractorItunes extends AbstractPlaylistExtractor { protected PlaylistExtractorItunes(PlaylistTypes playlistType, String playlistUrl, String[] playlistLines, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, List itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { Set itunesTrackNumbersSet groovy.util.Node playlistXmlNode = new PlaylistXmlParser(false, false).parse(playlistUrl) myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType) //if special iTunes playlists are demanded: looking for iTunes playlist-IDs, finding iTunes track numbers belonging to these IDs if (Playlist.isNullOrEmpty(itunesPlaylistNames)) { itunesTrackNumbersSet = [] } else { itunesTrackNumbersSet = findTrackNumbersItunes(playlistXmlNode, itunesPlaylistNames) } //logging only if (Playlist.isNullOrEmpty(itunesPlaylistNames)) { Playlist.logg(' - will try to cache all media tracks') } else if (Playlist.isNullOrEmpty(itunesTrackNumbersSet)) { Playlist.logg(' - will try to cache all media tracks (havn\'t found playlist(s) \'' + itunesPlaylistNames.toString() + '\')') } else { Playlist.logg(' - will try to cache ' + itunesTrackNumbersSet.size().toString() + ' media track(s) from iTunes playlist(s) \'' + itunesPlaylistNames.toString() + '\'') } //looking for mediaName and mediaUrl from tracks in the iTunes playlists; omitting disabled tracks playlistXmlNode['dict'][0]['dict'][0]['dict'].each { Map itunesTrackXmlElementMap = constructXmlElementItunesMap(it) //looking for mediaName and mediaUrl of each track, if its number is in itunesTrackNumberSet (or all tracks are required) if (Playlist.isNullOrEmpty(itunesTrackNumbersSet) || itunesTrackXmlElementMap['track id'] in itunesTrackNumbersSet) { if (Playlist.isNullOrFalse(itunesTrackXmlElementMap['disabled'])) { //specifying medium (mediaUrl will be UTF-8 decoding in ConstructMedium.correctMediaUrlSyntax) if (myMedium.setMediaUrl(itunesTrackXmlElementMap['location'].trim())) { PlaylistNameTags.ALBUMTITLE.setTagValuePlaylist(itunesTrackXmlElementMap['album']) PlaylistNameTags.TRACKTITLE.setTagValuePlaylist(itunesTrackXmlElementMap['name']) PlaylistNameTags.ARTIST.setTagValuePlaylist(findMediaNamePatternArtistItunes(itunesTrackXmlElementMap)) PlaylistNameTags.YEAR.setTagValuePlaylist(itunesTrackXmlElementMap['year']) mediaItems << myMedium.getMediaItem() myMedium.reset() } else { Playlist.logg('--- medium refused, seems not to be a valid URL') } } } } } /** * Correlates iTunes playlist names to tracknumbers, multiples will be excluded * @param playlistXmlNode: just investigated iTunes playlist XML node element * @param itunesPlaylistNames * @return LinkedHashSet */ private LinkedHashSet findTrackNumbersItunes(groovy.util.Node playlistXmlNode, List itunesPlaylistNames) { Set itunesTrackNumbersSet = new LinkedHashSet(Playlist.ESTIMATED_MEDIA_COUNT) playlistXmlNode['dict'][0]['array'][0]['dict'].each { Map itunesPlaylistXmlElementMap = constructXmlElementItunesMap(it) //collecting iTunes track numbers by iTunes playlist names (known from Serviio console) if (itunesPlaylistNames.any { it.toLowerCase() == itunesPlaylistXmlElementMap['name']?.toLowerCase()} && itunesPlaylistXmlElementMap['playlist id']?.size() > 0) { it['array'][0]['dict'].each { itunesTrackNumbersSet << (it['integer'][0] as groovy.util.Node).text() } } } return itunesTrackNumbersSet } /** * Reads iTunes key - value pairs in a HashMap * @param playlistXmlNode: just investigated iTunes playlist XML node element * @return Hashmap */ private Map constructXmlElementItunesMap(groovy.util.Node playlistXmlNode) { String keyElementName String keyElementValue Map itunesPlaylistXmlElementMap = new HashMap(ESTIMATED_XML_KEYS_COUNT_ITUNES, 1f) playlistXmlNode.depthFirst().eachWithIndex {groovy.util.Node v, Integer i -> if (i % 2) { keyElementName = v.name().toString().toLowerCase() keyElementValue = v.text().toLowerCase() } else if (i > 0 && keyElementName == 'key') { itunesPlaylistXmlElementMap << [(keyElementValue) : (Playlist.isNullOrEmpty(v.children()))? v.name() : v.text()] } } return itunesPlaylistXmlElementMap } private String findMediaNamePatternArtistItunes(Map itunesTrackXmlElementMap) { if (!Playlist.isNullOrEmpty(itunesTrackXmlElementMap['artist'])) { return itunesTrackXmlElementMap['artist'] } else if (!Playlist.isNullOrEmpty(itunesTrackXmlElementMap['album artist'])) { return itunesTrackXmlElementMap['album artist'] } else if (!Playlist.isNullOrEmpty(itunesTrackXmlElementMap['composer'])) { return itunesTrackXmlElementMap['composer'] } else { return null } } } private class PlaylistExtractorAsx extends AbstractPlaylistExtractor { protected PlaylistExtractorAsx(PlaylistTypes playlistType, String playlistUrl, String[] playlistLines, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, List itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { groovy.util.Node playlistXmlNode = new PlaylistXmlParser(false, false).parse(playlistUrl) myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType) testingSyntax: for (String entryElement in VALID_MEDIA_ELEMENTS_ASX) { if (!Playlist.isNullOrEmpty(playlistXmlNode[entryElement])) { playlistXmlNode[entryElement].each { //specifying medium if (myMedium.setMediaUrl(findMediaUrlASX(it))) { PlaylistNameTags.TRACKTITLE.setTagValuePlaylist(findMediaNamePatternTitleAsx(it)) PlaylistNameTags.ARTIST.setTagValuePlaylist(findMediaNamePatternArtistAsx(it)) mediaItems << myMedium.getMediaItem() myMedium.reset() } else { Playlist.logg('--- medium refused, seems not to be a valid URL') } } break testingSyntax } } } /** * Tries to extract mediaNamePattern(artist) from ASX playlists (using different spellings of element 'author') * @param entryNode: just investigated XML node * @return mediaName if found, else null string */ private String findMediaNamePatternArtistAsx(groovy.util.Node entryNode) { String mediaName = '' testingSyntax: for (titleElement in VALID_MEDIA_ARTIST_ELEMENTS_ASX) { if (!Playlist.isNullOrEmpty(entryNode[titleElement])) { mediaName = entryNode[titleElement].text() break testingSyntax } } return mediaName } /** * Tries to extract mediaNamePattern(track title) from ASX playlists (using different spellings of element 'title') * @param entryNode: just investigated XML node * @return mediaName if found, else null string */ private String findMediaNamePatternTitleAsx(groovy.util.Node entryNode) { String mediaName = '' testingSyntax: for (titleElement in VALID_MEDIA_TITLE_ELEMENTS_ASX) { if (!Playlist.isNullOrEmpty(entryNode[titleElement])) { mediaName = entryNode[titleElement].text() break testingSyntax } } return mediaName } /** * Looking for mediaUrl from ASX playlists (using different spellings of element 'ref' and attribute 'href') * @param entryNode: just investigated XML node * @return mediaUrl if found, else null */ private String findMediaUrlASX(groovy.util.Node entryNode) { String mediaUrl testingSyntax: for (refElement in VALID_MEDIA_URL_ELEMENTS_ASX) { for (hrefAttribute in VALID_MEDIA_URL_ATTRIBUTES_ASX) { if (!Playlist.isNullOrEmpty(entryNode[refElement][hrefAttribute])) { mediaUrl = (entryNode[refElement][hrefAttribute].getClass() == groovy.util.NodeList)? (entryNode[refElement][hrefAttribute][0]) : (entryNode[refElement][hrefAttribute]) break testingSyntax } } } return mediaUrl } } private class PlaylistExtractorSmil extends AbstractPlaylistExtractor { protected PlaylistExtractorSmil(PlaylistTypes playlistType, String playlistUrl, String[] playlistLines, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, List itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { groovy.util.Node playlistXmlNode = new PlaylistXmlParser(false, false).parse(playlistUrl) myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType) testingSyntax: for (String mediaElement in VALID_MEDIA_URL_ELEMENTS_SMIL) { if (!Playlist.isNullOrEmpty(playlistXmlNode['body']['seq'][mediaElement])) { playlistXmlNode['body']['seq'][mediaElement].each{ //specifying medium if (myMedium.setMediaUrl(it['@src'])) { PlaylistNameTags.TRACKTITLE.setTagValuePlaylist(findMediaNamePatternTitleSmil(it)) mediaItems << myMedium.getMediaItem() myMedium.reset() } else { Playlist.logg('--- medium refused, seems not to be a valid URL') } } break testingSyntax } } } /** * Tries to extract mediaName from SMIL playlists * @param entryNode: just investigated XML node * @return mediaName if found, else null string */ private String findMediaNamePatternTitleSmil(groovy.util.Node entryNode) { String mediaName = '' Object playlistXmlTitle = entryNode['@title'] if (!(Playlist.isNullOrEmpty(playlistXmlTitle))) { mediaName = playlistXmlTitle.text() } return mediaName } } private class PlaylistExtractorXspf extends AbstractPlaylistExtractor { protected PlaylistExtractorXspf(PlaylistTypes playlistType, String playlistUrl, String[] playlistLines, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, List itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { groovy.util.Node playlistXmlNode = new PlaylistXmlParser(false, false).parse(playlistUrl) myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType) playlistXmlNode['trackList'][0]['track'].each { //specifying medium if (myMedium.setMediaUrl((it['location'] as groovy.util.NodeList).text())) { PlaylistNameTags.ALBUMTITLE.setTagValuePlaylist((it['album'] as groovy.util.NodeList).text()) PlaylistNameTags.TRACKTITLE.setTagValuePlaylist((it['title'] as groovy.util.NodeList).text()) PlaylistNameTags.ARTIST.setTagValuePlaylist((it['artist'] as groovy.util.NodeList).text()) mediaItems << myMedium.getMediaItem() myMedium.reset() } else { Playlist.logg('--- medium refused, seems not to be a valid URL') } } } } /** * Constructs entries for each WebResourceItem after the playlist is read.
* First for each playlist once setPlaylistParameters has to be invoked.
* Second for each playlist entry setMediaUrl and if applicable setMediaName has to be invoked.
* Afterwards getMediaItems returns the WebResourceItem with all entries. Because of track numbering depends on track count * this part of processing is done in the AbstractPlaylistExtractor class. */ private class ConstructMedium { private static PlaylistTypes playlistType private static String playlistUrl private static String playlistThumbnailUrl private static String playlistName private static String playlistNamePattern private static PlaylistLiveStatus playlistLive private static MediaFileType playlistMediaType private String mediaUrl private String mediaNamePlaylist protected void setPlaylistParameters(PlaylistTypes playlistType, String playlistUrl, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) { this.playlistType = playlistType this.playlistUrl = playlistUrl this.playlistThumbnailUrl = playlistThumbnailUrl this.playlistName = playlistName this.playlistNamePattern = playlistNamePattern this.playlistLive = playlistLive this.playlistMediaType = playlistMediaType } protected Boolean setMediaUrl(String mediaUrlRaw) { if (mediaUrlRaw?.trim()) { Playlist.logg(' - original mediaUrl: ' + mediaUrlRaw) //correct mediaUrl mediaUrl = correctMediaUrl(mediaUrlRaw) if (!Playlist.isNullOrEmpty(mediaUrl)) { return true } } return false } protected void setMediaName(String mediaNamePlaylist) { this.mediaNamePlaylist = mediaNamePlaylist } protected WebResourceItem getMediaItem() { Boolean mediaLive = isMediaLive() String mediaName = constructMediaName() String mediaThumbnailUrl = findMediaThumbnailUrl() WebResourceItem mediaItem = new WebResourceItem(title: mediaName, additionalInfo: [(InfoItems.MEDIATHUMBNAILURL.getText()):mediaThumbnailUrl, (InfoItems.MEDIATYPE.getText()):playlistMediaType, (InfoItems.MEDIAURL.getText()):mediaUrl, (InfoItems.MEDIALIVE.getText()): mediaLive]) return mediaItem } protected void reset() { mediaNamePlaylist = '' PlaylistNameTags.ALBUMTITLE.reset() } private String findMediaThumbnailUrl() { String mediaThumbnailUrl if (!(mediaUrl ==~ Playlist.VALID_MEDIA_WEB_URL)) { //get thumbnail for local mediaUrl //get thumbnail for local mediaUrl from meta data String mediaUrlHash = mediaUrl.hashCode().toString() String coverArtPathFileNameTrunk = Playlist.TEMP_FOLDER + Playlist.VALID_COVER_ART_FILE_BEGIN + mediaUrlHash String VALID_COVER_ART_FILE_SPECIAL = Playlist.VALID_COVER_ART_FILE_BEGIN + mediaUrlHash + '(\\.jpg|\\.png)' File[] coverArtFilesSpecial = new File(Playlist.TEMP_FOLDER).listFiles(new FilenameFilter() { boolean accept (File fileDir, String fileName) { return fileName ==~ VALID_COVER_ART_FILE_SPECIAL } }) if (!Playlist.isNullOrEmpty(coverArtFilesSpecial)) { mediaThumbnailUrl = ("file:///" + coverArtFilesSpecial[0].getCanonicalPath()).replaceAll('\\\\', '/') Playlist.logg(' - mediaThumbnailUrl already known from meta data: ' + mediaThumbnailUrl) Playlist.removeFromCoverArtMap(coverArtFilesSpecial[0].getName()) } else { Boolean extractAndSaveCoverArtFileOk = CoverArtFileTypes.JFIF.extractAndSaveFile(mediaUrl, coverArtPathFileNameTrunk) if (extractAndSaveCoverArtFileOk) { String coverArtPathFileName = CoverArtFileTypes.getPathFileName() Playlist.removeFromCoverArtMap(coverArtPathFileName) mediaThumbnailUrl = ("file:///" + coverArtPathFileName).replaceAll('\\\\', '/') Playlist.logg(' - mediaThumbnailUrl new extracted from meta data: ' + mediaThumbnailUrl) CoverArtFileTypes.JFIF.reset() } } //get thumbnail for local mediaUrl from local cover art if (Playlist.isNullOrEmpty(mediaThumbnailUrl)) { try { File[] myFiles = new File(new File(mediaUrl).getParent()).listFiles() foundMediaThumbnailUrl: for (myJpg in [ 'albumartsmall\\.jpg', 'albumart.*small\\.jpg', 'albumart.*large\\.jpg', 'folder\\.jpg' ]) { for (myFile in myFiles) { if (myFile.getName().toLowerCase() ==~ myJpg) { mediaThumbnailUrl = myFile.toURI().toString() Playlist.logg(' - mediaThumbnailUrl from local cover art: ' + mediaThumbnailUrl) break foundMediaThumbnailUrl } } } } catch(e) { Playlist.logg(' - exception finding mediaThumbnailUrl in local files: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } } } //get thumbnail from playlistThumbnailUrl or standard web link if (Playlist.isNullOrEmpty(mediaThumbnailUrl)) { if (Playlist.isNullOrEmpty(playlistThumbnailUrl)) { mediaThumbnailUrl = 'http://www.veryicon.com/icon/png/System/Rhor%20v2%20Part%202/MP3%20File.png' Playlist.logg(' - mediaThumbnailUrl from standard web link: ' + mediaThumbnailUrl) } else { mediaThumbnailUrl = playlistThumbnailUrl Playlist.logg(' - mediaThumbnailUrl from playlistThumbnailUrl: ' + mediaThumbnailUrl) } } return mediaThumbnailUrl } private Boolean isMediaLive() { return (playlistLive == PlaylistLiveStatus.UNKNOWN)? (mediaUrl ==~ Playlist.VALID_MEDIA_WEB_URL) : (playlistLive == PlaylistLiveStatus.LIVE) } /** * Corrects mediaUrl and tries to complete relative URLs; tests for excistance of local files * @param playlistType * @param playlistUrl * @param mediaUrl * @return mediaUrl or null string, if not exists */ private String correctMediaUrl(String mediaUrlRaw) { String mediaUrlTemp1 = mediaUrlRaw Boolean foundMediaUrl = false //correcting mediaUrl: switching '\' to '/' mediaUrlTemp1 = mediaUrlTemp1.replaceAll('\\\\', '/') //correcting mediaUrl: file(n)= syntax in PLS if (playlistType == PlaylistTypes.PLS && mediaUrlTemp1 ==~ VALID_LINE_FILE_COMPLETE_PLS) { mediaUrlTemp1 = mediaUrlTemp1.substring(mediaUrlTemp1.indexOf('=') + 1).trim() } //correcting mediaUrl: dots while (mediaUrlTemp1.endsWith('.')) { mediaUrlTemp1 = mediaUrlTemp1.substring(0, mediaUrlTemp1.size() - 1) } //correcting mediaUrl: preceding 'file:' and '/', UTF-8 encoding mediaUrlTemp1 = correctMediaUrlSyntax(mediaUrlTemp1) //Web mediaUrl won't be tested if (mediaUrlTemp1 ==~ Playlist.VALID_MEDIA_WEB_URL) { foundMediaUrl = true Playlist.logg(' - mediaUrl interpreted as web URL') } else { Playlist.logg(' - mediaUrl interpreted as local URL') } //testing and trying to correct local mediaUrl testingLocalMediaUrl: while (!foundMediaUrl) { //testing original local mediaUrl if (new File(mediaUrlTemp1).isFile()) { foundMediaUrl = true break testingLocalMediaUrl } //trying to correct relativ pathes: simple addition String mediaUrlTemp2 = mediaUrlTemp1 while (mediaUrlTemp2.startsWith('/')) { mediaUrlTemp2 = mediaUrlTemp2.substring(1, mediaUrlTemp2.size()) } if ((new File(correctMediaUrlSyntax(playlistUrl.substring(0, playlistUrl.lastIndexOf('/')) + '/' + mediaUrlTemp2)).isFile())) { mediaUrlTemp1 = correctMediaUrlSyntax(playlistUrl.substring(0, playlistUrl.lastIndexOf('/')) + '/' + mediaUrlTemp2) foundMediaUrl = true Playlist.logg(' - corrected relative mediaUrl by addition') break testingLocalMediaUrl } //trying to correct relativ pathes: shared pattern String playlistUrlTemp2 = playlistUrl.substring(0, playlistUrl.lastIndexOf('/')) Matcher myMatcher = (playlistUrlTemp2.reverse() =~ '/') Integer playlistUrlMatchCounter = 0 Integer mediaUrlMatchCount = (mediaUrlTemp2 =~ '/').size() for (Integer i : myMatcher) { playlistUrlMatchCounter++ if (playlistUrlMatchCounter <= mediaUrlMatchCount) { if ((playlistUrlTemp2[playlistUrlTemp2.size() - myMatcher.start()..
  • original mediaName extracted from playlist or meta data (standard or individual composition, depending on transmitted playlistNamePattern)
  • *
  • mediaName constructed from playlistName
  • *
  • mediaName constructed from mediaUrl
  • */ private String constructMediaName() { String mediaNameTemp //mediaName from meta data or playlist for (PlaylistNameTags myPlaylistNameTag in PlaylistNameTags.values()) { if (playlistNamePattern.contains(myPlaylistNameTag.getAbbreviation())) { String myTagValue = myPlaylistNameTag.getTagValue(mediaUrl) if (!Playlist.isNullOrEmpty(myTagValue)) { if (mediaNameTemp == null) { mediaNameTemp = playlistNamePattern.replaceAll(myPlaylistNameTag.getAbbreviation(), myTagValue) } else { mediaNameTemp = mediaNameTemp.replaceAll(myPlaylistNameTag.getAbbreviation(), myTagValue) } } else { } } } if (!Playlist.isNullOrEmpty(mediaNameTemp)) { Playlist.logg(' - mediaName from meta data or playlist: ' + mediaNameTemp) } else if (!Playlist.isNullOrEmpty(mediaNamePlaylist)) { //mediaName from playlist mediaNameTemp = mediaNamePlaylist Playlist.logg(' - mediaName from playlist: ' + mediaNameTemp) } else if (!Playlist.isNullOrEmpty(playlistName)) { //mediaName from transmitted global playlistName mediaNameTemp = playlistName Playlist.logg(' - mediaName from playlistName: ' + mediaNameTemp) } else { //mediaName from mediaUrl mediaNameTemp = mediaUrl if (!(mediaUrl ==~ Playlist.VALID_MEDIA_WEB_URL)) { if (mediaNameTemp.indexOf('/') > -1) { mediaNameTemp = mediaNameTemp.substring(mediaNameTemp.lastIndexOf('/') + 1) } if (mediaNameTemp.indexOf('.') > 0) { mediaNameTemp = mediaNameTemp.substring(0, mediaNameTemp.lastIndexOf('.')) } } else { //try mediaName for web mediaUrl from mediaUrl mediaNameTemp = mediaUrl if (mediaNameTemp.indexOf('//') > -1) { mediaNameTemp = mediaNameTemp.substring(mediaNameTemp.indexOf('//') + 2) } if (mediaNameTemp.indexOf('/') > -1) { mediaNameTemp = mediaNameTemp.substring(0, mediaNameTemp.indexOf('/')) } if (mediaNameTemp.indexOf(':') > 0) { mediaNameTemp = mediaNameTemp.substring(0, mediaNameTemp.indexOf(':')) } } Playlist.logg(' - mediaName from mediaUrl: ' + mediaNameTemp) } return mediaNameTemp } } //* * * managing cover art * * * //* * * * ** * * * * * * * * * * static { if (Configuration.getTranscodingFolder().endsWith(File.separator)) { TEMP_FOLDER = Configuration.getTranscodingFolder() COVER_ART_LIST_PATH_NAME = TEMP_FOLDER + COVER_ART_LIST_NAME coverArtList = new File(COVER_ART_LIST_PATH_NAME) } else { TEMP_FOLDER = Configuration.getTranscodingFolder() + File.separator COVER_ART_LIST_PATH_NAME = TEMP_FOLDER + COVER_ART_LIST_NAME coverArtList = new File(COVER_ART_LIST_PATH_NAME) performWrongCoverArtCleaning() } performCoverArtList() deleteCoverArtList() writeCoverArtListFromScratch() } private static void performWrongCoverArtCleaning() { File wrongCoverArtList = new File(Configuration.getTranscodingFolder() + COVER_ART_LIST_NAME) if (wrongCoverArtList.isFile()) { try { wrongCoverArtList.delete() } catch (e) { } } try { File[] wrongCoverArtFiles = new File(Configuration.getTranscodingFolder().substring(0, Configuration.getTranscodingFolder().lastIndexOf(File.separator))).listFiles(new WrongCoverArtFileNameFilter()) for (File wrongCoverArtFile in wrongCoverArtFiles) { try { wrongCoverArtFile.delete() } catch(e) { } } } catch (e) { } } private static void performCoverArtList() { if (coverArtList.isFile()) { try { String[] coverArtLines = new BufferedReader(new FileReader(coverArtList)).readLines().toArray() for (coverArtLine in coverArtLines) { try { Boolean tempBoolean = new File(TEMP_FOLDER + coverArtLine).delete() logg(' - deleted cover art file: ' + TEMP_FOLDER + coverArtLine + ', ' + tempBoolean) } catch (e) { } } } catch (e) { Playlist.logg(' - exception performCoverArtList: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } finally { logg('--- ready deleting cover art files') } } } private static void deleteCoverArtList() { if (coverArtList.isFile()) { try { Boolean tempBoolean = coverArtList.delete() logg(' - deleted cover art list: ' + coverArtList.toString() + ', ' + tempBoolean) } catch (e) { Playlist.logg(' - exception deleteCoverArtList: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } } } private static void writeCoverArtListFromScratch() { FileWriter myFileWriter try { File[] coverArtFiles = new File(TEMP_FOLDER).listFiles(new CoverArtFileNameFilter()) myFileWriter = new FileWriter(COVER_ART_LIST_PATH_NAME) for (File coverArtFile in coverArtFiles) { myFileWriter.write(coverArtFile.getName() + '\n') logg(' - write in cover art list from scratch: ' + coverArtFile.getName()) } } catch (e) { Playlist.logg(' - exception writeCoverArtListFromScratch: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } finally { myFileWriter.close() logg('--- ready writing in cover art list from scratch') } } private static void readCoverArtList() { try { coverArtListMap.clear() new BufferedReader(new FileReader(coverArtList)).readLines().each {v -> String tempString = coverArtListMap.put(org.codehaus.groovy.runtime.DefaultGroovyMethods.toInteger(v.substring(VALID_COVER_ART_FILE_BEGIN.size(), v.lastIndexOf('.'))), v) logg(' - read from cover art list: ' + v + ', ' + tempString)} } catch (e) { Playlist.logg(' - exception readCoverArtList: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } finally { logg('--- ready reading from cover art list') } } private static void removeFromCoverArtMap(String filePathName) { Integer key = org.codehaus.groovy.runtime.DefaultGroovyMethods.toInteger(filePathName.substring(filePathName.indexOf(VALID_COVER_ART_FILE_BEGIN) + VALID_COVER_ART_FILE_BEGIN.size(), filePathName.lastIndexOf('.'))) String tempString = coverArtListMap.remove(key) logg(' - removed from cover art map: ' + filePathName + ', ' + tempString) } private static void writeCoverArtList() { FileWriter myFileWriter try { coverArtList.delete() myFileWriter = new FileWriter(COVER_ART_LIST_PATH_NAME) coverArtListMap.values().each {v -> myFileWriter.write(v + '\n') } } catch (e) { Playlist.logg(' - exception writeCoverArtList: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } finally { myFileWriter.close() } } private static class CoverArtFileNameFilter implements FilenameFilter { boolean accept (File fileDir, String fileName) { return fileName ==~ VALID_COVER_ART_FILE } } private static class WrongCoverArtFileNameFilter implements FilenameFilter { boolean accept (File fileDir, String fileName) { return fileName ==~ (Configuration.getTranscodingFolder().substring(Configuration.getTranscodingFolder().lastIndexOf(File.separator) + 1, Configuration.getTranscodingFolder().size()) + VALID_COVER_ART_FILE) } } //* * * technical methods and classes * * * //* * * * * * * * * * * * * * * * * * * * * /** * Central logging function also for inner classes * @param message */ private static Logger MY_LOGGER = LoggerFactory.getLogger(FeedItemUrlExtractor.class) private static void logg(String message) { MY_LOGGER.debug(String.format('%1$s: %2$s', [ 'Playlist extractor', message] as Object[])) } /** * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ private static Boolean isNullOrEmpty(CharSequence cs) { int strLen if (cs == null || (strLen = cs.length()) == 0) { return true } for (int i in 0..by janko */ private static class KMPMatch { /** * Finds the first occurrence of the pattern in the text. */ protected static Integer indexOf(Byte[] data, Byte[] pattern) { Integer[] failure = computeFailure(pattern) Integer j = 0 if (data.length == 0) return Integer.MAX_VALUE as Integer for (Integer i in 0.. 0 && pattern[j] != data[i]) { j = failure[j - 1] } try { if (pattern[j] == data[i]) { j++; } if (j == pattern.length) { return i - pattern.length + 1 } } catch (e) { } } return Integer.MAX_VALUE as Integer } /** * Computes the failure function using a boot-strapping process, * where the pattern is matched against itself. */ private static Integer[] computeFailure(Byte[] pattern) { Integer[] failure = new Integer[pattern.length] //Integer[] will be initialized with null instead of 0 Arrays.fill(failure, 0 as Integer) Integer j = 0 for (Integer i in 1.. 0 && pattern[j] != pattern[i]) { j = failure[j - 1] } if (pattern[j] == pattern[i]) { j++ } failure[i] = j } return failure } } private static class ThisPlatform { private static Platforms getPlatform() { if (System.getProperty('os.name').toString() ==~ Playlist.VALID_OS_NAME_WINDOWS) { return Platforms.WINDOWS } else if (System.getProperty('os.name').toString() ==~ Playlist.VALID_OS_NAME_MAC) { return Platforms.MAC } else { return Platforms.UNIXOID } } } //* * * enums * * * //* * * * * * * * * private enum Platforms {WINDOWS, UNIXOID, MAC} private enum PlaylistTypes {M3U(''), EXTM3U('#extm3u'), PLS('[playlist]'), ASX('asx'), ITUNES('plist'), SMIL('smil'), XSPF('playlist'), ERROR_XML_UNKNOWN(''), ERROR_XML_WRONG_ENCODED(''), ERROR_XML_PARSER(''), ERROR_XML_IO('') private String signature private PlaylistTypes(String signature) { this.signature = signature } protected String getSignature() { return signature } } private enum PlaylistLiveStatus {LIVE, NOTLIVE, UNKNOWN protected static PlaylistLiveStatus getStatus(String playlistLiveStatus) { if (LIVE.name().equalsIgnoreCase(playlistLiveStatus)) { return LIVE } else if (NOTLIVE.name().equalsIgnoreCase(playlistLiveStatus)) { return NOTLIVE } else { return UNKNOWN } } } private enum InfoItems {MEDIATHUMBNAILURL('mediaThumbnailUrl'), MEDIATYPE('mediaType'), MEDIAURL('mediaUrl'), MEDIALIVE('mediaLive') private String itemText private InfoItems(String itemText) { this.itemText = itemText } protected String getText() { return itemText } } private enum PlaylistUrlProperties {URL('u'), PICTURE('p'), NAME('n'), NAMEPATTERN('a'), LIVE_STATUS('l'), TYPE('t'), ITUNES_PL_NAME('i'), NUMBERING('m') private String abbreviation private PlaylistUrlProperties(String abbreviation) { this.abbreviation = abbreviation } protected String getAbbreviation() { return abbreviation } } private enum PlaylistNameTags { ALBUMTITLE('-at', ['ALBUM', 'ORIGINAL_ALBUM'] as String[], null, null), TRACKTITLE('-tt', ['TITLE'] as String[], null, null), ARTIST('-ar', [ 'ARTIST', 'ARTISTS', 'ALBUM_ARTIST', 'ORIGINAL_ARTIST', 'LYRICIST', 'ORIGINAL_LYRICIST', 'COMPOSER', 'CONDUCTOR'] as String[], null, null), COMPOSER('-co', ['COMPOSER'] as String[], null, null), YEAR('-y', ['YEAR', 'ORIGINAL_YEAR']as String[], null, null) private String abbreviation private List jFieldKeys = new ArrayList(0) private String tagValuePlaylist private String tagValueMetadata private PlaylistNameTags(String abbreviation, String[] jFieldKeyStrings, tagValuePlaylist, tagValueMetadata) { this.abbreviation = abbreviation jFieldKeyStrings.eachWithIndex {v, i -> try { this.jFieldKeys[i] = FieldKey.valueOf(v) } catch(e) { } } //this.jFieldKeys = jFieldKeys this.tagValuePlaylist = tagValuePlaylist this.tagValueMetadata = tagValueMetadata } protected void reset() { for (PlaylistNameTags playlistNameTag in PlaylistNameTags.values()) { playlistNameTag.tagValuePlaylist = null playlistNameTag.tagValueMetadata = null } } protected String getAbbreviation() { return abbreviation } protected void setTagValuePlaylist(String tagValuePlaylist) { this.tagValuePlaylist = tagValuePlaylist } protected String getTagValue(String mediaUrl) { String tempValue = getTagValueMetadata(mediaUrl) if (tempValue == null) { if (Playlist.isNullOrEmpty(tagValuePlaylist)) { return null } else { return tagValuePlaylist } } else { return tempValue } } private String getTagValueMetadata(String mediaUrl) { try { AudioFile myAudioFile = AudioFileIO.read(new File(mediaUrl)) for (FieldKey myFieldKey in jFieldKeys) { Tag myTag = myAudioFile.getTag() String mediaNameFragment = myTag.getFirst(myFieldKey) if (!Playlist.isNullOrEmpty(mediaNameFragment)) { return mediaNameFragment.trim() } } return null } catch(e) { if (mediaUrl ==~ Playlist.VALID_MEDIA_WEB_URL) { Playlist.logg(' - exception jaudiotagger at online mediaUrl: ' + mediaUrl) } else { Playlist.logg(' - exception jaudiotagger (' + jFieldKeys.toString() + '): ' + mediaUrl) Playlist.logg(' - exception jaudiotagger (' + jFieldKeys.toString() + '): ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } return null } } } private enum CoverArtFileTypes { JFIF('.jpg', [0XFF, 0XD8, 0XFF]as List, Integer.MAX_VALUE), JNG('.jpg', [ 0X8B, 0X4A, 0X4E, 0X47, 0X0D, 0X0A, 0X1A, 0X0A] as List, Integer.MAX_VALUE), PNG('.png', [ 0X89, 0X50, 0X4E, 0X47, 0X0D, 0X0A, 0X1A, 0X0A] as List, Integer.MAX_VALUE) private String fileExtension private Byte[] magicNumber private Integer startIndex private static String coverArtPathFileName private CoverArtFileTypes(String fileExtension, List magicNumber, Integer startIndex) { this.fileExtension = fileExtension this.magicNumber = magicNumber this.startIndex = startIndex } protected void reset() { for (CoverArtFileTypes coverArtFileType in CoverArtFileTypes.values()) { coverArtFileType.startIndex = Integer.MAX_VALUE } } protected Boolean extractAndSaveFile(String mediaUrl, String coverArtPathFileNameTrunk) { try { //looking for raw cover art bytes AudioFile myAudioFile = AudioFileIO.read(new File(mediaUrl)) Tag myTag = myAudioFile.getTag() Byte[] coverArtArray = myTag.getFirstField(FieldKey.COVER_ART).getRawContent() //looking for suitable file type try { if (!Playlist.isNullOrEmpty(coverArtArray)) { foundSuitableFileType: for (CoverArtFileTypes coverArtFileType in CoverArtFileTypes.values()) { coverArtFileType.startIndex = KMPMatch.indexOf(coverArtArray, coverArtFileType.magicNumber) if (coverArtFileType.startIndex < Playlist.ESTIMATED_MAX_NUMBER_COVER_ART_HEAD_BYTES) { break foundSuitableFileType } } //looking for smallest startIndex CoverArtFileTypes fittingFileType = JFIF for (CoverArtFileTypes coverArtFileType in CoverArtFileTypes.values()) { if (coverArtFileType.startIndex < fittingFileType.startIndex) { fittingFileType = coverArtFileType } } //writing cover art file try { if (fittingFileType.startIndex != Integer.MAX_VALUE) { coverArtPathFileName = coverArtPathFileNameTrunk + fittingFileType.fileExtension FileOutputStream myStream = new FileOutputStream(coverArtPathFileName) myStream.write(coverArtArray as byte[], fittingFileType.startIndex, coverArtArray.length - fittingFileType.startIndex) myStream.close() return true } } catch (e) { Playlist.logg(' - exception FileOutputStream: ' + mediaUrl) Playlist.logg(' - exception FileOutputStream: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } } } catch (e) { Playlist.logg(' - exception KMPMatch: ' + mediaUrl) Playlist.logg(' - exception KMPMatch: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } } catch(e) { Playlist.logg(' - exception jaudiotagger: ' + mediaUrl) Playlist.logg(' - exception jaudiotagger: ' + e.getMessage() + '\n' + e.toString() + '\n' + e.getStackTrace().toString()) } return false } protected static String getPathFileName() { return coverArtPathFileName } } }