//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:
* - it handles playlists like web sites, so the playlists need an entry at Serviio consoles 'Online source' tab
* - source type here has to be 'Web Resource'
* - the plugin behavior is managed by different switches
* Principle construction of Serviio console 'Online sources' - 'Source URL' entries:
* - http://switch-1{entry for switch-1}[switch-2{entry for switch-2}...[switch-n{entry for switch-n}]]]
* - there must be NO spaces or other separators between 'http://' and the first switch or between the switches
* - there must be slashes instead of backslashes, for Windows pathes also
*
* Principle construction of each switch:
* - -SWITCH-x or shortcut -SW-x
* - where x determines the type of switch
*
- naming is case sensitive
* - sequence of switches doesn't matter
*
*
* Meaning and use of the different switches:
* - -SWITCH-u or -SW-u:
* - indicates the path of the playlist file
* - is required
* - entry is allowed in one of the following syntaxes (slashes instead of backslashes!):
* - http(s):// ...
* - file:///{letter local drive}:/ ... (Windows only)
* - file:///{name server}/ ... (Windows only)
* - file:/// ...
* - entry should be UTF-8 encoded, i.e. requires '%20' instead of space
* - example: -SW-ufile:///c:/Users/Smith/My%20Music/TestPlaylist.m3u
* - -SWITCH-p or -SW-p:
* - indicates the path of the thumbnail file
* - is optional
* - only used if no cover art file is readable from file meta data; in case of a local playlist: only used if no cover art file is inside the tracks folders
* - requirements are the same as for -SW-u
* - example: -SW-phttp://www.veryicon.com/icon/png/System/Rhor%20v2%20Part%202/MP3%20File.png
* - -SWITCH-a or -SW-a:
* - indicates a pattern for individual naming the media entries
* - is optional
* - entry should be UTF-8 encoded, i.e. requires '%20' instead of space
* - following wildcards are possible:
* - -at: album title
* - -y: year of production
* - -ar: artist
* - -tt: track title
* - -co: composer
* - default pattern is '-ar%20-%20-tt' if the switch -SW-a is omitted
* - wildcard substitutions are taken from playlist entries or meta data
* - if none of the demanded substitutions can be taken neither from playlist entries nor from meta data following backup mechanism for media name will taking place: 'Title' entry in pls or extm3u playlists >
* entry -SW-n (see below) > media name constructed from the URL
* - example: -SW-a-ar:%20-tt%20(-at%20-%20-y) will be resolved to 'The Beatles: A day in the life (Sgt. Pepper's Lonly Hearts Club Band - 1967)'
* - -SWITCH-n or -SW-n:
* - indicates a default common name for all media titles in the playlist
* - is optional
* - only for back up reasons when naming the media entries (see above: -SW-a)
* - entry should be UTF-8 encoded, i.e. requires '%20' instead of space
* - example: -SW-nLove%20Songs
* - -SWITCH-m or -SW-m:
* - indicates to omit automatic track numbering
* - is optional
* - no special entry possible
* - example: -SW-m
* - -SWITCH-t or -SW-t:
* - indicates the media type of the entries in the playlist
* - is optional but SHOULD BE USED
* - if not available the plugin decides depending on the media name extension (this desicion may be wrong!)
* - all entries inside a playlist must be of the same type, i.e. VIDEO or AUDIO (no mixing possible)
* - the radio buttons 'Audio' and 'Video' at Serviio console 'Enter details of online source' mask must be set in the same sence;
* they do decide in which list of the renderer (i.e. TV) the playlist is listed; BUT they do not decide which type of
* media is contained inside the playlist in matters of the plugin and Serviios file streaming
* - examples: -SW-tAUDIO or -SW-tVIDEO (the only possible entries)
* - -SWITCH-l or -SW-l ('l' = small letter 'L'):
* - indicates whether the playlist entries are live tracks or not
* - is optional
* - if not available the plugin decides: every local medium is not live, every online medium is live
* - examples: -SW-lLIVE or -SW-lNOTLIVE (the only possible entries)
* - -SWITCH-i or -SW-i:
* - indicates which iTunes subplaylist(s) in its general playlist 'iTunes Music Library.xml' should be played
* - only used in case of iTunes playlists
* - is optional
* - can be used several times for multiple iTunes subplaylists inside 'iTunes Music Library.xml'
* - if not available or entry -SW-iALL: all entries of the iTunes playlist are played, regardless of which iTunes subplaylist it belongs to
* - entry(ies) should be UTF-8 encoded, i.e. requires '%20' instead of space
* - example: -SW-iKlassische%20Musik-SW-iJazz
*
*
* Noticed extensions of the playlists:
* - .m3u
* - .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)
* - .asx
* - .wpl
* - .pls
* - .xspf
* - .smi
* - .smil
*
* Possible formats for the media paths inside a playlist:
* - valid url including scheme and scheme specific part (http://..., mms://... etc.)
* - [file:]{letter local drive}:{path to playlist}.{extension} (Windows only; '\' or '/': both work)
* - [file:]//{name server}|localhost/{share name of drive}{path to playlist}.{extension} (Windows only; '\' or '/': both work)
* - [file:]{path to playlist from scratch}.{extension} (Linux only; '\' or '/': both work)
* - relative pathes starting from playlist path
*
* 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
}
}
}