//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.*

/**
 * <h3>Serviio 'playlist' extractor plugin for formats: m3u, extm3u, pls, asx, smil, itunes, xspf</h3><br>
 * <b>Some generell remarks on using the plugin:</b>
 * <ul><li>it handles playlists like web sites, so the playlists need an entry at Serviio consoles 'Online source' tab</li>
 * <li>source type here has to be 'Web Resource'</li>
 * <li>the plugin behavior is managed by different switches</li></ul><br>
 * <b>Principle construction of Serviio console 'Online sources' - 'Source URL' entries:</b>
 * <ul><li>http://switch-1{entry for switch-1}[switch-2{entry for switch-2}...[switch-n{entry for switch-n}]]]</li>
 *     <li>there must be NO spaces or other separators between 'http://' and the first switch or between the switches</li>
 *     <li>there must be slashes instead of backslashes, for Windows pathes also</li></ul>
 * <br>
 * <b>Principle construction of each switch:</b>
 * <ul><li>-SWITCH-x or shortcut -SW-x</li>
 *     <li>where x determines the type of switch</b>
 *     <li>naming is case sensitive</li>
 *     <li>sequence of switches doesn't matter</li> 
 * </ul>
 * <br>
 * <b>Meaning and use of the different switches:</b>
 * <ul><li>-SWITCH-u or -SW-u:</li>
 *         <ul><li>indicates the path of the playlist file</li>
 *         <li>is required</li>
 *         <li>entry is allowed in one of the following syntaxes (slashes instead of backslashes!):</li>
 *             <ul><li>http(s):// ...</li>
 *             <li>file:///{letter local drive}:/ ... (Windows only)</li>
 *             <li>file:///{name server}/ ... (Windows only)</li>
 *             <li>file:/// ...</li></ul>
 *         <li>entry should be UTF-8 encoded, i.e. requires '%20' instead of space</li>
 *         <li>example: -SW-ufile:///c:/Users/Smith/My%20Music/TestPlaylist.m3u</li></ul>
 *     <li>-SWITCH-p or -SW-p:</li>
 *         <ul><li>indicates the path of the thumbnail file</li>
 *         <li>is optional</li>
 *         <li>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</li>
 *         <li>requirements are the same as for -SW-u</li>
 *         <li>example: -SW-phttp://www.veryicon.com/icon/png/System/Rhor%20v2%20Part%202/MP3%20File.png</li></ul>
 *     <li>-SWITCH-a or -SW-a:</li>
 *         <ul><li>indicates a pattern for individual naming the media entries</li>
 *         <li>is optional</li>
 *         <li>entry should be UTF-8 encoded, i.e. requires '%20' instead of space</li>
 *         <li>following wildcards are possible:</li>
 *             <ul><li>-at: album title</li>
 *             <li>-y: year of production</li>
 *             <li>-ar: artist</li>
 *             <li>-tt: track title</li>
 *             <li>-co: composer</li></ul>
 *         <li>default pattern is '-ar%20-%20-tt' if the switch -SW-a is omitted</li>
 *         <li>wildcard substitutions are taken from playlist entries or meta data</li>
 *         <li>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 &gt; 
 *             entry -SW-n (see below) &gt; media name constructed from the URL</li>
 *         <li>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)'</li></ul>
 *     <li>-SWITCH-n or -SW-n:</li>
 *         <ul><li>indicates a default common name for all media titles in the playlist</li>
 *         <li>is optional</li>
 *         <li>only for back up reasons when naming the media entries (see above: -SW-a)</li>
 *         <li>entry should be UTF-8 encoded, i.e. requires '%20' instead of space</li>
 *         <li>example: -SW-nLove%20Songs</li></ul>
 *     <li>-SWITCH-m or -SW-m:</li>
 *         <ul><li>indicates to omit automatic track numbering</li>
 *         <li>is optional</li>
 *         <li>no special entry possible</li>
 *         <li>example: -SW-m</li></ul>
 *     <li>-SWITCH-t or -SW-t:</li>
 *         <ul><li>indicates the media type of the entries in the playlist</li>
 *         <li>is optional but SHOULD BE USED</li>
 *         <li>if not available the plugin decides depending on the media name extension (this desicion may be wrong!)</li>
 *         <li>all entries inside a playlist must be of the same type, i.e. VIDEO or AUDIO (no mixing possible)</li>
 *         <li>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</li> 
 *         <li>examples: -SW-tAUDIO or -SW-tVIDEO (the only possible entries)</li></ul>
 *     <li>-SWITCH-l or -SW-l ('l' = small letter 'L'):</li>
 *         <ul><li>indicates whether the playlist entries are live tracks or not</li>
 *         <li>is optional</li>
 *         <li>if not available the plugin decides: every local medium is not live, every online medium is live</li>
 *         <li>examples: -SW-lLIVE or -SW-lNOTLIVE (the only possible entries)</li></ul>
 *     <li>-SWITCH-i or -SW-i:</li>
 *         <ul><li>indicates which iTunes subplaylist(s) in its general playlist 'iTunes Music Library.xml' should be played</li>
 *         <li>only used in case of iTunes playlists</li>
 *         <li>is optional</li>
 *         <li>can be used several times for multiple iTunes subplaylists inside 'iTunes Music Library.xml'</li>
 *         <li>if not available or entry -SW-iALL: all entries of the iTunes playlist are played, regardless of which iTunes subplaylist it belongs to</li>
 *         <li>entry(ies) should be UTF-8 encoded, i.e. requires '%20' instead of space</li>
 *         <li>example: -SW-iKlassische%20Musik-SW-iJazz</li></ul>
 * </ul>       
 * <br>
 * <b>Noticed extensions of the playlists:</b>
 * <ol><li>.m3u</li>
 *     <li>.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)</li>
 *     <li>.asx</li>
 *     <li>.wpl</li>
 *     <li>.pls</li>
 *     <li>.xspf</li>
 *     <li>.smi</li>
 *     <li>.smil</li>
 * </ol>
 * <b>Possible formats for the media paths inside a playlist:</b> 
 * <ol><li>valid url including scheme and scheme specific part (http://..., mms://... etc.) </li>
 *     <li>[file:]{letter local drive}:{path to playlist}.{extension} (Windows only; '\' or '/': both work) </li>
 *     <li>[file:]//{name server}|localhost/{share name of drive}{path to playlist}.{extension} (Windows only; '\' or '/': both work) </li>
 *     <li>[file:]{path to playlist from scratch}.{extension} (Linux only; '\' or '/': both work) </li>
 *     <li>relative pathes starting from playlist path</li>
 * </ol>
 * <b>Tested for</b><br>
 * Windows 7 and QNAP Linux
 * <br>
 * 
 * @author Olaf Ahrens
 * @see <a href="http://gonze.com/playlists/playlist-format-survey.html">playlist information by Lucas Gonze</a> 
 * @see <a href="http://xspf.org/">reference xspf playlist format</a> 
 * @see <a href="http://msdn.microsoft.com/de-de/library/ms910265.aspx">reference asx elements</a>
 * @see <a href="http://www.w3.org/TR/2008/REC-SMIL3-20081201/">reference smil playlist format</a>
 * @see <a href="http://www.jthink.net/jaudiotagger/index.jsp">org.jaudiotagger by Paul Taylor</a> 
 * @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<Integer, String> coverArtListMap = new HashMap<Integer, String>(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
		}
	}


	@Override
	protected boolean expiresImmediately() {
		return true
	}


	/**
	 * Extracts a given playlist named by its URL<br>
	 * <br>
	 * 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<String> 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<WebResourceItem>)
	}


	/**
	 * 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://httpmedia.radiobremen.de/bremeneins.m3u'
		Playlist pl = new Playlist()

		assert pl.extractorMatches(new URL(testUrl))
		WebResourceContainer container = pl.extractItems(new URL('http://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<String> itunesPlaylistNames = new ArrayList<String>(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<String> 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<WebResourceItem> mediaItems = new ArrayList<WebResourceItem>(Playlist.ESTIMATED_MEDIA_COUNT)
		protected ConstructMedium myMedium = new ConstructMedium()

		protected ArrayList<WebResourceItem> 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<String> itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) {

			myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType)

			for (Integer i in 0..<playlistLines.length) {
				Playlist.logg('  - playlistLine no: ' + i.toString() + ', content: ' + playlistLines[i])

				//skip empty lines, comments and PLS technical lines
				if (!(NO_VALID_PLAYLIST_LINES_M3U.any{playlistLines[i].trim() ==~ it} || playlistLines[i].trim().size() == 0)) {
					//specifying medium
					if (myMedium.setMediaUrl(playlistLines[i].trim())) {
						mediaItems << myMedium.getMediaItem()
						myMedium.reset()
					} else {
						Playlist.logg('--- medium refused, seems not to be a valid URL')
					}
				}
			}
		}
	}


	private class PlaylistExtractorExtm3u extends AbstractPlaylistExtractor {

		protected PlaylistExtractorExtm3u(PlaylistTypes playlistType, String playlistUrl, String[] playlistLines, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, List<String> itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) {

			String mediaName

			myMedium.setPlaylistParameters(playlistType, playlistUrl, playlistThumbnailUrl, playlistName, playlistNamePattern, playlistLive, playlistMediaType)

			for (Integer i in 0..<playlistLines.length) {
				Playlist.logg('  - playlistLine no: ' + i.toString() + ', content: ' + playlistLines[i])

				//skip empty lines, comments and PLS technical lines
				if (!(NO_VALID_PLAYLIST_LINES_EXTM3U.any{playlistLines[i].trim() ==~ it} || playlistLines[i].trim().size() == 0)) {
					//retrieving mediaName from EXTM3U #EXTINF line
					if (playlistLines[i].startsWith('#EXTINF')) {
						mediaName = playlistLines[i].substring(playlistLines[i].indexOf(',') + 1)
					} else if (!(playlistLines[i].startsWith('#'))) {
						//specifying medium
						if (myMedium.setMediaUrl(playlistLines[i].trim())) {
							myMedium.setMediaName(mediaName)
							mediaItems << myMedium.getMediaItem()
							mediaName = ''
							myMedium.reset()
						} else {
							Playlist.logg('--- medium refused, seems not to be a valid URL')
						}
					}
				}
			}
		}
	}


	private class PlaylistExtractorPls extends AbstractPlaylistExtractor {

		protected PlaylistExtractorPls(PlaylistTypes playlistType, String playlistUrl, String[] playlistLines, String playlistThumbnailUrl, String playlistName, String playlistNamePattern, List<String> 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<String> itunesPlaylistNames, PlaylistLiveStatus playlistLive, MediaFileType playlistMediaType) {

			Set<String> 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<String, String> 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<String> findTrackNumbersItunes(groovy.util.Node playlistXmlNode, List<String> itunesPlaylistNames) {

			Set<String> itunesTrackNumbersSet = new LinkedHashSet<String>(Playlist.ESTIMATED_MEDIA_COUNT)

			playlistXmlNode['dict'][0]['array'][0]['dict'].each {
				Map<String, String> 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<String, String> constructXmlElementItunesMap(groovy.util.Node playlistXmlNode) {

			String keyElementName
			String keyElementValue
			Map<String, String> itunesPlaylistXmlElementMap = new HashMap<String, String>(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<String, String> 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<String> 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<String> 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<String> 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.<br>
	 * First for each playlist once setPlaylistParameters has to be invoked.<br>
	 * Second for each playlist entry setMediaUrl and if applicable setMediaName has to be invoked.<br>
	 * 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()..<playlistUrlTemp2.size()] + '/') == mediaUrlTemp2[0..myMatcher.start()]) {
							if (new File(correctMediaUrlSyntax(playlistUrlTemp2[0..playlistUrlTemp2.size() - myMatcher.start() - 2] + '/' + mediaUrlTemp2)).isFile()) {
								mediaUrlTemp1 = correctMediaUrlSyntax(playlistUrlTemp2[0..playlistUrlTemp2.size() - myMatcher.start() - 2] + '/' + mediaUrlTemp2)
								foundMediaUrl = true
								Playlist.logg('  - corrected relative mediaUrl by pattern match')
								break testingLocalMediaUrl
							}
						}
					}
				}
				break testingLocalMediaUrl
			}

			return foundMediaUrl? mediaUrlTemp1 : ''
		}


		/**
		 * Corrects mediaUrl syntax
		 * @param mediaUrl: from playlist extracted mediaUrl
		 * @return corrected mediaUrl
		 */
		private String correctMediaUrlSyntax(String mediaUrlRaw) {

			String mediaUrlTemp = mediaUrlRaw

			//UTF-8 decoding of local mediaUrl
			if (!(mediaUrlTemp ==~ Playlist.VALID_MEDIA_WEB_URL)) {
				mediaUrlTemp = URLDecoder.decode(mediaUrlTemp, 'UTF-8')
			}
			
			//correcting mediaUrl: delete 'file:' (not nessesary)
			if (mediaUrlTemp ==~ VALID_MEDIA_FILE_PROTOCOL) {
				mediaUrlTemp = mediaUrlTemp.substring(5, mediaUrlTemp.size())
			}

			//correcting mediaUrl: preceding '/' and 'localhost/C of iTunes
			if (ThisPlatform.getPlatform() != Platforms.WINDOWS) {
				while (mediaUrlTemp.startsWith('//')) {
					mediaUrlTemp = mediaUrlTemp.substring(1, mediaUrlTemp.size())
				}
			} else {
				while ((mediaUrlTemp ==~ '///.*') || (mediaUrlTemp ==~ '/' + VALID_WINDOWS_DRIVE_LETTER + '.*')) {
					mediaUrlTemp = mediaUrlTemp.substring(1, mediaUrlTemp.size())
				}
				if (mediaUrlTemp ==~ '/{0,2}.*/' + VALID_WINDOWS_DRIVE_LETTER + '.*') {
					Matcher myMatcher = (mediaUrlTemp =~ '/' + VALID_WINDOWS_DRIVE_LETTER)
					myMatcher.find()
					mediaUrlTemp = mediaUrlTemp.substring(myMatcher.start() + 1, mediaUrlTemp.size())
				}
			}

			return mediaUrlTemp
		}


		/**
		 * Constructs mediaName
		 * @param playlistName: for constructing mediaName if this is not extracted from playlist or media meta data
		 * @param mediaName: string if extracted from playlist or meta data, else null string
		 * @param mediaUrl: for constructing mediaName if this is not extracted from playlist or meta data
		 * @return <ol><li>original mediaName extracted from playlist or meta data (standard or individual composition, depending on transmitted playlistNamePattern)</li>
		 *    <li>mediaName constructed from playlistName</li>
		 *    <li>mediaName constructed from mediaUrl</li></ol>
		 */
		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..<strLen) {
			if (Character.isWhitespace(cs.charAt(i)) == false) {
				return false
			}
		}
		return true
	}
	private static Boolean isNullOrEmpty(Collection myCollection) {
		if (myCollection instanceof groovy.util.Node) {
			return isNullOrEmpty(myCollection[0])
		} else {
			return (myCollection == null || myCollection.isEmpty())
		}
	}
	private static Boolean isNullOrEmpty(groovy.util.Node myNode) {
		return (myNode == null || (Playlist.isNullOrEmpty(myNode.children()) && Playlist.isNullOrEmpty(myNode.attributes())))
	}
	private static Boolean isNullOrEmpty(Object[] myArray) {
		return (myArray == null || myArray.size() == 0)
	}


	private static Boolean isNullOrFalse(String myString) {
		return (myString == null || myString.trim().toLowerCase() == 'false')
	}
	private static Boolean isNullOrFalse(groovy.util.Node myNode) {
		return myNode == null
	}


	/**
	 * Overrides groovy.util.XmlParser.parse(boolean validating, boolean namespaceAware) in case of offline mode:
	 * DTDs won't be reachable, e.g. in case of iTunes playlists.
	 */
	private class PlaylistXmlParser extends groovy.util.XmlParser {

		PlaylistXmlParser(boolean validating, boolean namespaceAware) {
			super(validating, namespaceAware)
		}

		@Override
		public groovy.util.Node parse(String uri) {
			try {
				super.parse(uri)
			} catch(IOException e) {
				super.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd', false)
				super.setFeature('http://xml.org/sax/features/namespaces', false)
				super.parse(uri)
			}
		}
	}


	/**
	 * Modified Knuth-Morris-Pratt Algorithm for Pattern Matching
	 * @see <a href="http://stackoverflow.com/questions/1507780/searching-for-a-sequence-of-bytes-in-a-binary-file-with-java">by janko</a>
	 */
	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..<data.length) {
				while (j > 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..<pattern.length) {
				while (j > 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<FieldKey> jFieldKeys = new ArrayList<FieldKey>(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<Byte>, Integer.MAX_VALUE),
		JNG('.jpg', [
			0X8B,
			0X4A,
			0X4E,
			0X47,
			0X0D,
			0X0A,
			0X1A,
			0X0A]
		as List<Byte>, Integer.MAX_VALUE),
		PNG('.png', [
			0X89,
			0X50,
			0X4E,
			0X47,
			0X0D,
			0X0A,
			0X1A,
			0X0A]
		as List<Byte>, Integer.MAX_VALUE)

		private String fileExtension
		private Byte[] magicNumber
		private Integer startIndex
		private static String coverArtPathFileName

		private CoverArtFileTypes(String fileExtension, List<Byte> 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
		}
	}
}
