def test_reuse_parser(verbose=False):
    parser0 = itunessmart.Parser(testdata[0]["info"], testdata[0]["criteria"])
    parser1 = itunessmart.Parser(testdata[1]["info"], testdata[1]["criteria"])

    # Reuse parser 0 with data[1]
    parser0.update_data_base64(testdata[1]["info"], testdata[1]["criteria"])

    assert parser0.result.queryTree == parser1.result.queryTree

    # Reuse parser 0 with data[3]
    parser0.update_data_base64(testdata[3]["info"], testdata[3]["criteria"])

    # Reuse parser 1 with data[3]
    parser1.update_data_base64(testdata[3]["info"], testdata[3]["criteria"])

    assert parser0.result.queryTree == parser1.result.queryTree
def test_xsp_minimal(verbose=False):
    library = readLibrary("library_minimal.xml")
    
    playlist = library['Playlists'][1]
    assert playlist['Name'] == "Chip - League of My Own II"
    
    parser = itunessmart.Parser()
    parser.update_data_bytes(playlist['Smart Info'],playlist['Smart Criteria'])
    
    assert parser.result.query == "(lower(Album) = 'league of my own ii')"
    
    files = itunessmart.createXSPFile(directory=".", name=playlist['Name'], smartPlaylist=parser.result, createSubplaylists=True)
    
    assert len(files) == 1
    
    with open(files[0], "r") as f:
        xsp = f.read()
    expected = """<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<smartplaylist type="songs">
    <name>Chip - League of My Own II</name>
    <match>all</match>
    <rule field="album" operator="is">
        <value>League of My Own II</value>
    </rule>


</smartplaylist>"""
    assert xsp == expected
    
    # Clean up
    for file in files:
        if os.path.isfile(file):
            os.remove(file)
def test_double_parse(verbose=False):
    parser = itunessmart.Parser(testdata[0]["info"], testdata[0]["criteria"])
    query = parser.result.query
    parser._parser.parse()
    assert query == parser.result.query
    parser._parser.parse()
    assert query == parser.result.query
def test_examples(verbose=False):
    for test in copy.deepcopy(testdata):
        parser = itunessmart.Parser(test["info"], test["criteria"])
        result = parser.result
        if verbose:
            print("\n\nTest: %s" % test["desc"])
            print("############")
            print(result.output)
            print("############")
            print(result.query)
            print("############")
            print(json.dumps(result.queryTree, indent=2))
            print("############")
            print(result.ignore)

        if "expected" in test and test["expected"]:
            exp = test["expected"]
            if "output" in exp:
                assert exp.pop("output") == result.output
            if "query" in exp:
                assert exp.pop("query") == result.query
            if "queryTree" in exp:
                assert exp.pop("queryTree") == result.queryTree
            if "ignore" in exp:
                assert exp.pop("ignore") == result.ignore

            for key in exp:
                assert exp[key] == result.queryTree[key]
        else:
            raise NotImplementedError(test["desc"])
def test_xsp_errors(verbose=False):
    parser = itunessmart.Parser(testdata[13]["info"], testdata[13]["criteria"])
    
    assert parser.result.query == "(MediaKind != 'Home Video')"
    
    try:
        files = itunessmart.createXSP(name="example", smartPlaylist=parser.result, createSubplaylists=False)
    except Exception as e:
        assert type(e) == itunessmart.xsp.PlaylistException
def test_library_minimal(verbose=False):
    library = readLibrary("library_minimal.xml")
    
    playlist = library['Playlists'][1]
    assert 'Name' in playlist and 'Smart Criteria' in playlist and 'Smart Info' in playlist and playlist['Smart Criteria']
    assert playlist['Name'] == "Chip - League of My Own II"
    
    parser = itunessmart.Parser()
    parser.update_data_bytes(playlist['Smart Info'],playlist['Smart Criteria'])
    
    assert parser.result.query == "(lower(Album) = 'league of my own ii')"
    
    
    mapping = itunessmart.generatePersistentIDMapping(library)
    assert mapping['38822E892B8D332A'] == "Chip - League of My Own II"
def test_xsp_subplaylists(verbose=False):
    library = readLibrary("library_onlysmartplaylists.xml")
    persistentIDMapping = itunessmart.generatePersistentIDMapping(library)
    parser = itunessmart.Parser()
    files = []
    for playlist in library['Playlists']:
        if 'Name' in playlist:
            if 'HipHop' in playlist['Name'] or 'rap' in playlist['Name']:
                parser.update_data_bytes(playlist['Smart Info'],playlist['Smart Criteria'])
                files += itunessmart.createXSPFile(directory='.', name=playlist['Name'], smartPlaylist=parser.result, createSubplaylists=True, persistentIDMapping=persistentIDMapping)
    

    with open("HipHop.xsp", "r") as f:
        xsp = f.read()
        
        # Rule: is playlist
        assert '''<rule field="playlist" operator="is">
        <value>Deutschrap</value>
    </rule>''' in xsp
        
        # Subplaylists match:
        rules = xsp.split('<rule field="playlist"')[1:]
        for rule in rules:
            subname = rule.split('<value>')[1].split('</value>')[0]
            filename = "%s.xsp" % subname
            assert os.path.isfile(filename)
    
    # XML escaped character
    with open("HipHop2000.xsp", "r") as f:
        xsp = f.read()
        name = xsp.split("<name>")[1].split("</name>")[0].strip()
        
        assert name == "HipHop&lt;2000"
    
    # Rule: global OR
    with open("Deutschrap.xsp", "r") as f:
        xsp = f.read()
        assert "<match>one</match>" in xsp
    
    
    # Clean up
    for file in files:
        if os.path.isfile(file):
            os.remove(file)
def main(iTunesLibraryFile=None, outputDirectory=None):

    if iTunesLibraryFile == "--help" or iTunesLibraryFile == "-h" or iTunesLibraryFile == "/?":
        print("""Export smart playlists from 'iTunes Music Library.xml' to .xsp files for Kodi.
This interactive script uses the default iTunes library, but you may also specify a xml file:

Optional arguments:

python3 -m itunessmart {iTunesLibraryFile} {outputDirectory}

{iTunesLibraryFile}\t - Default is ~\\Music\\iTunes\\iTunes Music Library.xml
{outputDirectory}\t - Default is ./out/""")
        return

    export_sub_playlists = True

    if not iTunesLibraryFile:
        home = os.path.expanduser("~")
        folder = os.path.join(home, "Music/iTunes")
        iTunesLibraryFile = os.path.join(folder, "iTunes Music Library.xml")
    elif not os.path.isfile(iTunesLibraryFile) and os.path.isfile(os.path.join(iTunesLibraryFile, "iTunes Music Library.xml")):
        iTunesLibraryFile = os.path.join(iTunesLibraryFile, "iTunes Music Library.xml")

    while not os.path.isfile(iTunesLibraryFile):
        iTunesLibraryFile = input("# Please enter the path of your `iTunes Music Library.xml`: ")
        if os.path.isfile(iTunesLibraryFile):
            break
        elif os.path.isfile(os.path.join(iTunesLibraryFile, "iTunes Music Library.xml")):
            iTunesLibraryFile = os.path.join(iTunesLibraryFile, "iTunes Music Library.xml")
            break
        else:
            print("! Could not find file `%s`")

    print("# Reading %s . . . " % iTunesLibraryFile)
    with open(iTunesLibraryFile, "rb") as fs:
        # Read XML file
        library = itunessmart.readiTunesLibrary(fs)
        persistentIDMapping = itunessmart.generatePersistentIDMapping(library)

    print("# Library loaded!")

    userinput = input("# Do you want to convert a (single) or (all) playlists? ")
    export_all = True
    if userinput.lower() in ("single", "1", "one"):
        export_all = False

    userinput = input("# Do you want to export nested rules to sub-playlists? (yes/no) ")
    if userinput.lower() in ("n", "no", "0"):
        export_sub_playlists = False

    # Decode and export all smart playlists

    parser = itunessmart.Parser()

    if outputDirectory is None:
        outputDirectory = os.path.abspath("out")

    if not os.path.exists(outputDirectory):
        os.makedirs(outputDirectory)

    if export_all:
        print("# Converting playlists to %s" % outputDirectory)

    res = []
    for playlist in library['Playlists']:
        if 'Name' in playlist and 'Smart Criteria' in playlist and 'Smart Info' in playlist and playlist['Smart Criteria']:
            try:
                parser.update_data_bytes(playlist['Smart Info'], playlist['Smart Criteria'])
            except Exception as e:
                print("! Failed to decode playlist:")
                try:
                    print(traceback.format_exc())
                    printWithoutException(playlist['Name'])
                except KeyError:
                    printWithoutException(playlist)

            if not parser.result:
                continue

            try:
                res.append((playlist['Name'], parser.result))
                if export_all:
                    itunessmart.createXSPFile(directory=outputDirectory, name=playlist['Name'], smartPlaylist=parser.result, createSubplaylists=export_sub_playlists, persistentIDMapping=persistentIDMapping)
            except itunessmart.EmptyPlaylistException as e:
                printWithoutException("! `%s` is empty." % playlist['Name'])
            except itunessmart.PlaylistException as e:
                printWithoutException("! Skipped `%s`: %s" % (playlist['Name'], str(e)))
            except Exception as e:
                print("! Failed to convert playlist:")
                try:
                    print(traceback.format_exc())
                    printWithoutException(playlist['Name'])
                except KeyError:
                    printWithoutException(playlist)

    if not export_all:
        i = 1
        for p_name, _ in res:
            printWithoutException("# %02d: %s" % (i, p_name))
            i += 1

        while True:
            i = input("# Please give the number of the playlist (0 to exit): ")
            if i in ("0", ""):
                break

            i = int(i)
            p_name, p_result = res[i-1]
            printWithoutException("# Converting Playlist: %s" % p_name)
            try:
                itunessmart.createXSPFile(directory=outputDirectory, name=p_name, smartPlaylist=p_result, createSubplaylists=export_sub_playlists, persistentIDMapping=persistentIDMapping)
            except itunessmart.EmptyPlaylistException as e:
                printWithoutException("! `%s` is empty." % p_name)
            except itunessmart.PlaylistException as e:
                printWithoutException("! Skipped `%s`: %s" % (p_name, str(e)))
            except Exception as e:
                try:
                    print("! Failed to convert playlist:")
                    print(traceback.format_exc())
                    printWithoutException(p_name)
                except (UnicodeEncodeError, KeyError, TypeError) as e:
                    print(p_result)

    print("# All done!")
    userdata = os.path.expandvars(r"%appdata%\kodi\userdata\playlists\music") if os.path.exists(os.path.expandvars(r"%appdata%\kodi\userdata")) else os.path.expanduser(r"~/Library/Application Support/Kodi/userdata/playlists/music")
    print("# You may copy the .xsp files to %s" % userdata)
#### Include modules from parent directory ###
import sys
import os
include = os.path.relpath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, include)

#### Example: ###

import itunessmart
import json

info = "AQEAAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
criteria = "U0xzdAABAAEAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAABAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/FNMc3QAAQABAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEAAAAAAAAABIAAAAAAAAAAAAAAAAAAAABAAAAAAAAABIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAABZAAAAAAAAAAAAAAAAAAAAAQAAAAAAAABZAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAA"
parser = itunessmart.Parser(info, criteria)
result = parser.result
print("############ Output:")
print(result.output)
print("############ Query:")
print(result.query)
print("############ JSON:")
print(json.dumps(result.queryTree, indent=2))
print("############ Ignore:")
print(result.ignore)