From 45563c821cfa9c9f3bbc9c3bc541b85c94f3f715 Mon Sep 17 00:00:00 2001 From: Remy Date: Sun, 7 Aug 2011 21:18:42 -0700 Subject: [PATCH] Added descriptions from last fm, formatted album page, added retry buttons --- data/css/style.css | 39 +++++++++-- data/interfaces/default/album.html | 38 ++++++---- data/interfaces/default/artist.html | 13 +++- data/interfaces/default/base.html | 2 +- data/interfaces/default/config.html | 2 +- data/interfaces/default/history.html | 7 +- data/interfaces/default/manage.html | 38 ++++++++-- data/interfaces/default/searchresults.html | 70 +++++++++++++++++++ data/interfaces/default/shutdown.html | 2 +- data/interfaces/default/upcoming.html | 18 +++-- headphones/__init__.py | 1 + headphones/importer.py | 2 + headphones/lastfm.py | 45 +++++++++++- headphones/mb.py | 41 +++++++++++ headphones/webserve.py | 80 +++++++--------------- 15 files changed, 306 insertions(+), 92 deletions(-) create mode 100644 data/interfaces/default/searchresults.html diff --git a/data/css/style.css b/data/css/style.css index 9bd44217..50614f91 100755 --- a/data/css/style.css +++ b/data/css/style.css @@ -102,15 +102,15 @@ a.blue { container { } -body { background-color: #EBF4FB; min-width: 907px; } +body { background-color: #EBF4FB; min-width: 930px; } -header { min-height: 68px; width: 100%; min-width: 907px; padding-left: 0px; padding-right: 10px; background-color: #CDC9C9; position: fixed; z-index: 998; } +header { min-height: 68px; width: 100%; min-width: 930px; padding-left: 0px; padding-right: 10px; background-color: #CDC9C9; position: fixed; z-index: 998; } h1 { font-size: 24px; } h2 { font-size: 20px; } h3 { font-size: 16px; } -p.indented { margin-left: 20px; font-size: 14px; } +p.indented { padding-top: 20px; margin-left: 20px; font-size: 14px; } div#updatebar { text-align: center; min-width: 970px; width: 100%; background-color: light-blue; float: left; } div#logo { float: left; padding-left: 10px; } @@ -120,7 +120,7 @@ ul#nav li { margin: 40px 0px auto 10px; display: inline; } ul#nav li a { padding: 5px; font-size: 16px; font-weight: bold; color: #330000; text-decoration: none; } ul#nav li a:hover { background-color: #a3e532; } -div#subhead_container { height: 30px; width:100%; min-width: 907px; background-color:#330000; float: left; list-style-type: none; z-index: 998; overflow: hidden; } +div#subhead_container { height: 30px; width:100%; min-width: 930px; background-color:#330000; float: left; list-style-type: none; z-index: 998; overflow: hidden; } ul#subhead_menu { margin-top: 5px; } ul#subhead_menu li { width: 100%; height: 100%; display: inline; } ul#subhead_menu li a { padding: 5px 15px 10px 15px; vertical-align: middle; color: white; font-size: 16px; text-decoration: none; } @@ -131,6 +131,9 @@ div#searchbar { margin: 24px 30px auto auto; float: right; } div#main { margin: 0; padding: 80px 0 0 0; } .table_wrapper { border-radius: 20px; -webkit-border-radius: 20px; -moz-border-radius: 20px; width: 88%; margin: 20px auto 0 auto; padding: 25px; background-color: white; position: relative; min-height: 200px; clear: both; _height: 302px; zoom: 1; } +.manage_wrapper { width: 88%; margin: 20px auto 0 auto; padding: 25px; min-height: 150px; clear: both; _height: 302px; zoom: 1; } +.table_wrapper_left { padding: 25px; background-color: #ffffff; float: left; width: 40%; height: 200px; margin-top: 25px; margin-left: 30px; margin-right: auto; -moz-border-radius: 20px; border-radius: 20px; } +.table_wrapper_right{ padding: 25px; background-color: #ffffff; width: 40%; height: 200px; margin-top: 25px; margin-left: auto; margin-right: 30px; -moz-border-radius: 20px; border-radius: 20px; } table#artist_table { background-color: white; width: 100%; padding: 20px; } @@ -144,6 +147,7 @@ table#artist_table td#album { vertical-align: middle; text-align: left; min-widt table#artist_table td#have { vertical-align: middle; } div#paddingheader { padding-top: 48px; font-size: 24px; font-weight: bold; text-align: center; } +div#nopaddingheader { font-size: 24px; font-weight: bold; text-align: center; } table#album_table { background-color: white; } table#album_table th#select { vertical-align: middle; text-align: left; min-width: 25px; } @@ -161,15 +165,15 @@ table#album_table td#type { vertical-align: middle; text-align: center; } table#album_table td#have { vertical-align: middle; } img.albumArt { float: left; padding-right: 5px; } -div#albumheader { height: 200px; } +div#albumheader { padding-top: 48px; height: 200px; } div#track_wrapper { padding-top: 20px; text-align: center; font-size: 16px; } -table#track_table th#number { text-align: left; min-width: 50px; } +table#track_table th#number { text-align: right; min-width: 20px; } table#track_table th#name { text-align: center; min-width: 350px; } table#track_table th#duration { width: 175px; text-align: center; min-width: 100px; } table#track_table th#have { width: 175px; text-align: center; min-width: 100px; } -table#track_table td#number { vertical-align: middle; text-align: left; } +table#track_table td#number { vertical-align: middle; text-align: right; } table#track_table td#name { vertical-align: middle; text-align: center; } table#track_table td#duration { vertical-align: middle; text-align: center; } table#track_table td#have { vertical-align: middle; text-align: center; } @@ -182,6 +186,27 @@ table#log_table th#timestamp { text-align: left; min-width: 165px; } table#log_table th#level { text-align: left; min-width: 75px; } table#log_table th#message { text-align: left; min-width: 200px; } +table#upcoming_table th#albumart { text-align: center; min-width: 50px; } +table#upcoming_table th#albumname { text-align: center; min-width: 200px; } +table#upcoming_table th#artistname { text-align: center; min-width: 150px; } +table#upcoming_table th#reldate { text-align: center; min-width: 100px; } +table#upcoming_table th#type { text-align: center; min-width: 75px; } + +table#upcoming_table td#albumart { vertical-align: middle; text-align: center; min-width: 50px; } +table#upcoming_table td#albumname { vertical-align: middle; text-align: center; min-width: 200px; } +table#upcoming_table td#artistname { vertical-align: middle; text-align: center; min-width: 150px; } +table#upcoming_table td#reldate { vertical-align: middle; text-align: center; min-width: 100px; } +table#upcoming_table td#type { vertical-align: middle; text-align: center; min-width: 75px; } +table#upcoming_table td#status { vertical-align: middle; text-align: center; } + +table#searchresults_table th#albumname { text-align: left; min-width: 225px; } +table#searchresults_table th#artistname { text-align: center; min-width: 325px; } +table#searchresults_table th#score { text-align: center; min-width: 75px; } + +table#searchresults_table td#albumname { vertical-align: middle; text-align: left; min-width: 200px; } +table#searchresults_table td#artistname { vertical-align: middle; text-align: left; min-width: 300px; } +table#searchresults_table td#score { vertical-align: middle; text-align: center; min-width: 75px; } + div.progress-container { border: 1px solid #ccc; width: 100px; height: 14px; margin: 2px 5px 2px 0; padding: 1px; float: left; background: white; } div.progress-container > div { background-color: #a3e532; height: 14px; } .havetracks { font-size: 13px; margin-left: 36px; padding-bottom: 3px; vertical-align: middle; } diff --git a/data/interfaces/default/album.html b/data/interfaces/default/album.html index e242556d..6fda2b27 100644 --- a/data/interfaces/default/album.html +++ b/data/interfaces/default/album.html @@ -7,26 +7,36 @@ <%def name="headerIncludes()">
<%def name="body()">
+

<- Back to ${album['ArtistName']}

albumart

${album['AlbumTitle']}

${album['ArtistName']}


+ <% + totalduration = myDB.action("SELECT SUM(TrackDuration) FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0] + totaltracks = len(myDB.select("SELECT TrackTitle from tracks WHERE AlbumID=?", [album['AlbumID']])) + %> +

Tracks: ${totaltracks}

+

Duration: ${helpers.convert_milliseconds(totalduration)}

+ %if description:

Description:

+ ${description['Summary']} + %endif
@@ -35,14 +45,18 @@ - + + <% + i = 0 + %> %for track in tracks: <% - trackmatch = 'asd' - if len(trackmatch): + i += 1 + have = myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND AlbumTitle like ? AND TrackTitle like ?', [album['ArtistName'], album['AlbumTitle'], track['TrackTitle']]) + if len(have): grade = 'A' check = 'checkmark' else: @@ -50,9 +64,9 @@ check = '' %> - + - + %endfor diff --git a/data/interfaces/default/artist.html b/data/interfaces/default/artist.html index 934d173a..9ded40dc 100644 --- a/data/interfaces/default/artist.html +++ b/data/interfaces/default/artist.html @@ -26,6 +26,7 @@

Mark selected albums as @@ -74,7 +75,15 @@

- + %endfor @@ -110,7 +119,7 @@ "sInfoEmpty":"Showing 0 to 0 of 0 albums", "sInfoFiltered":"(filtered from _MAX_ total albums)"}, "bPaginate": false, - "aaSorting": [[3, 'asc'],[2,'desc']] + "aaSorting": [[4, 'asc'],[3,'desc']] }); }); diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index dc89306c..3704a6c2 100755 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -49,7 +49,7 @@
  • settings
  • # Track Title DurationHave
    #${i} ${track['TrackTitle']}${track['TrackDuration']}${helpers.convert_milliseconds(track['TrackDuration'])} ${check}
    ${album['AlbumTitle']} ${album['ReleaseDate']} ${album['Type']}${album['Status']}${album['Status']} + %if album['Status'] == 'Skipped': + [want] + %elif album['Status'] == 'Wanted': + [skip] + %else: + [retry][new] + %endif +
    ${havetracks}/${totaltracks}
    @@ -216,5 +217,4 @@

    - <%inherit file="base.html" /> \ No newline at end of file diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html index f1002609..77d80d87 100644 --- a/data/interfaces/default/history.html +++ b/data/interfaces/default/history.html @@ -1,4 +1,7 @@ <%inherit file="base.html"/> +<%! + from headphones import helpers +%> <%def name="headerIncludes()">
    @@ -22,6 +25,7 @@
    + @@ -39,8 +43,9 @@ - + + %endfor diff --git a/data/interfaces/default/manage.html b/data/interfaces/default/manage.html index 0d91e773..28d9e015 100644 --- a/data/interfaces/default/manage.html +++ b/data/interfaces/default/manage.html @@ -1,4 +1,8 @@ <%inherit file="base.html" /> +<%! + import headphones +%> + <%def name="body()">

    Scan Music Library


    @@ -10,24 +14,44 @@ as soon as you click 'Submit'

    + %if headphones.MUSIC_DIR: + + %else: + + %endif
    -
    + +

    Import Last.FM Artists


    Enter the username whose artists you want to import:

    - -

    -

    Placeholder :-)


    +

    +
    + +
    +

    Placeholder :-)




    -


    - ''' % (music_dir_input, lastfm_user_text)) + Check for Headphones Updates


    + \ No newline at end of file diff --git a/data/interfaces/default/searchresults.html b/data/interfaces/default/searchresults.html new file mode 100644 index 00000000..5109d10b --- /dev/null +++ b/data/interfaces/default/searchresults.html @@ -0,0 +1,70 @@ +<%inherit file="base.html" /> + +<%def name="body()"> + +
    +

    Search Results

    +

    +
    File Name Size Status
    ${item['DateAdded']} ${item['Title']}${item['Size']}${helpers.bytes_to_mb(item['Size'])} ${item['Status']}[retry][new]
    + + + %if type == 'album': + + %endif + + + + + + + %if searchresults: + %for result in searchresults: + <% + if result['score'] == 100: + grade = 'A' + else: + grade = 'Z' + %> + + %if type == 'album': + + %endif + + + %if type == 'album': + + %else: + + %endif + + %endfor + %endif + +
    Album NameArtist NameScore
    ${result['title']}${result['uniquename']}${result['score']}Add this albumAdd this artist
    + + +<%def name="headIncludes()"> + + + +<%def name="javascriptIncludes()"> + + + \ No newline at end of file diff --git a/data/interfaces/default/shutdown.html b/data/interfaces/default/shutdown.html index 8a882e39..917be6b2 100644 --- a/data/interfaces/default/shutdown.html +++ b/data/interfaces/default/shutdown.html @@ -6,6 +6,6 @@ <%def name="body()">
    - Headphones is ${message} +

    Headphones is ${message}

    \ No newline at end of file diff --git a/data/interfaces/default/upcoming.html b/data/interfaces/default/upcoming.html index 086d8c6f..ffde5a35 100644 --- a/data/interfaces/default/upcoming.html +++ b/data/interfaces/default/upcoming.html @@ -9,7 +9,7 @@ Artist Album Name Release Date - Release Type + Type Status @@ -26,30 +26,38 @@ %endfor -
    + +
    +

    Mark selected albums as + + +

    Wanted Albums

    + - - + %for album in wanted: + - %endfor diff --git a/headphones/__init__.py b/headphones/__init__.py index 84023254..112cb4ce 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -429,6 +429,7 @@ def dbcheck(): c.execute('CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT)') c.execute('CREATE TABLE IF NOT EXISTS have (ArtistName TEXT, AlbumTitle TEXT, TrackNumber TEXT, TrackTitle TEXT, TrackLength TEXT, BitRate TEXT, Genre TEXT, Date TEXT, TrackID TEXT)') c.execute('CREATE TABLE IF NOT EXISTS lastfmcloud (ArtistName TEXT, ArtistID TEXT, Count INTEGER)') + c.execute('CREATE TABLE IF NOT EXISTS descriptions (ReleaseGroupID TEXT, ReleaseID TEXT, Summary TEXT, Content TEXT)') try: c.execute('SELECT IncludeExtras from artists') diff --git a/headphones/importer.py b/headphones/importer.py index a64e959e..88d5b350 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -215,6 +215,8 @@ def addArtisttoDB(artistid, extrasonly=False): myDB.upsert("albums", newValueDict, controlValueDict) + lastfm.getAlbumDescription(rg['id'], release_dict['releaselist']) + # I changed the albumid from releaseid -> rgid, so might need to delete albums that have a releaseid for release in release_dict['releaselist']: myDB.action('DELETE from albums WHERE AlbumID=?', [release['releaseid']]) diff --git a/headphones/lastfm.py b/headphones/lastfm.py index a3e9c8ec..914bd2b8 100644 --- a/headphones/lastfm.py +++ b/headphones/lastfm.py @@ -4,7 +4,7 @@ from collections import defaultdict import random import headphones -from headphones import db +from headphones import db, logger api_key = '395e6ec6bb557382fc41fde867bce66f' @@ -91,4 +91,47 @@ def getArtists(): for artistid in artistlist: importer.addArtisttoDB(artistid) +def getAlbumDescription(rgid, releaselist): + + myDB = db.DBConnection() + result = myDB.select('SELECT Summary from descriptions WHERE ReleaseGroupID=?', [rgid]) + + if result: + return + + for release in releaselist: + + mbid = release['releaseid'] + url = 'http://ws.audioscrobbler.com/2.0/?method=album.getInfo&mbid=%s&api_key=%s' % (mbid, api_key) + logger.info('Checking last.fm for: ' + mbid) + data = urllib.urlopen(url).read() + + if data == 'Album not found': + logger.info('Release id not on last fm, skipping') + continue + + try: + d = minidom.parseString(data) + + albuminfo = d.getElementsByTagName("album") + + for item in albuminfo: + summarynode = item.getElementsByTagName("summary")[0].childNodes + contentnode = item.getElementsByTagName("content")[0].childNodes + for node in summarynode: + summary = node.data + for node in contentnode: + content = node.data + + controlValueDict = {'ReleaseGroupID': rgid} + newValueDict = {'ReleaseID': mbid, + 'Summary': summary, + 'Content': content} + myDB.upsert("descriptions", newValueDict, controlValueDict) + logger.info('Inserted description') + break + + except: + continue + \ No newline at end of file diff --git a/headphones/mb.py b/headphones/mb.py index 16a9da71..92723fda 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -66,6 +66,47 @@ def findArtist(name, limit=1): }) return artistlist + +def findRelease(name, limit=1): + + with mb_lock: + + releaselist = [] + attempt = 0 + releaseResults = None + + chars = set('!?') + if any((c in chars) for c in name): + name = '"'+name+'"' + + while attempt < 5: + + try: + releaseResults = q.getReleases(ws.ReleaseFilter(query=name, limit=limit)) + break + except WebServiceError, e: + logger.warn('Attempt to query MusicBrainz for %s failed: %s' % (name, e)) + attempt += 1 + time.sleep(5) + + time.sleep(1) + + if not releaseResults: + return False + + for result in releaseResults: + + releaselist.append({ + 'uniquename': result.release.artist.name, + 'title': result.release.title, + 'id': u.extractUuid(result.release.artist.id), + 'albumid': u.extractUuid(result.release.id), + 'url': result.release.artist.id, + 'albumurl': result.release.id, + 'score': result.score + }) + + return releaselist def getArtist(artistid, extrasonly=False): diff --git a/headphones/webserve.py b/headphones/webserve.py index a812b1a9..cb6a98f9 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -10,7 +10,7 @@ import threading import headphones -from headphones import logger, searcher, db, importer, lastfm +from headphones import logger, searcher, db, importer, mb, lastfm from headphones.helpers import checked, radio @@ -50,140 +50,112 @@ class WebInterface(object): myDB = db.DBConnection() album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone() tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [AlbumID]) + description = myDB.action('SELECT * from descriptions WHERE ReleaseGroupID=?', [AlbumID]).fetchone() title = album['ArtistName'] + ' - ' + album['AlbumTitle'] - return serve_template(templatename="album.html", title=title, album=album, tracks=tracks) + return serve_template(templatename="album.html", title=title, album=album, tracks=tracks, description=description) albumPage.exposed = True def search(self, name, type): - if len(name) == 0: raise cherrypy.HTTPRedirect("home") if type == 'artist': - searchresults = mb.findArtist(name, limit=10) + searchresults = mb.findArtist(name, limit=20) else: - searchresults = mb.findRelease(name, limit=10) - - findArtist.exposed = True - - def artistInfo(self, artistid): - page = [templates._header] - page.append(templates._logobar) - page.append(templates._nav) - artist = mb.getArtist(artistid) - if artist['artist_begindate']: - begindate = artist['artist_begindate'] - else: - begindate = '' - if artist['artist_enddate']: - enddate = artist['artist_enddate'] - else: - enddate = '' - page.append('''

    Artist Information:

    ''') - page.append('''

    Artist Name: %s (%s)
    ''' % (artist['artist_name'], artist['artist_type'])) - page.append('''

    Years Active: %s - %s

    ''' % (begindate, enddate)) - page.append('''MusicBrainz Link: http://www.musicbrainz.org/artist/%s

    Albums:
    ''' % (artistid, artistid)) - for rg in artist['releasegroups']: - page.append('''%s
    ''' % rg['title']) - page.append('''

    ''' % artistid) - return page - - artistInfo.exposed = True + searchresults = mb.findRelease(name, limit=20) + return serve_template(templatename="searchresults.html", title='Search Results for: "' + name + '"', searchresults=searchresults, type=type) + search.exposed = True def addArtist(self, artistid): - threading.Thread(target=importer.addArtisttoDB, args=[artistid]).start() time.sleep(5) threading.Thread(target=lastfm.getSimilar).start() raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % artistid) - addArtist.exposed = True def getExtras(self, ArtistID): - myDB = db.DBConnection() controlValueDict = {'ArtistID': ArtistID} newValueDict = {'IncludeExtras': 1} myDB.upsert("artists", newValueDict, controlValueDict) - threading.Thread(target=importer.addArtisttoDB, args=[ArtistID, True]).start() time.sleep(10) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) - getExtras.exposed = True def pauseArtist(self, ArtistID): - logger.info(u"Pausing artist: " + ArtistID) myDB = db.DBConnection() controlValueDict = {'ArtistID': ArtistID} newValueDict = {'Status': 'Paused'} myDB.upsert("artists", newValueDict, controlValueDict) - - raise cherrypy.HTTPRedirect("home") - + raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) pauseArtist.exposed = True def resumeArtist(self, ArtistID): - logger.info(u"Resuming artist: " + ArtistID) myDB = db.DBConnection() controlValueDict = {'ArtistID': ArtistID} newValueDict = {'Status': 'Active'} myDB.upsert("artists", newValueDict, controlValueDict) - - raise cherrypy.HTTPRedirect("home") - + raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) resumeArtist.exposed = True def deleteArtist(self, ArtistID): - logger.info(u"Deleting all traces of artist: " + ArtistID) myDB = db.DBConnection() myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from albums WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from tracks WHERE ArtistID=?', [ArtistID]) - raise cherrypy.HTTPRedirect("home") - deleteArtist.exposed = True def refreshArtist(self, ArtistID): importer.addArtisttoDB(ArtistID) + raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) refreshArtist.exposed=True def markAlbums(self, ArtistID=None, action=None, **args): myDB = db.DBConnection() + if action == 'WantedNew': + newaction = 'Wanted' + else: + newaction = action for mbid in args: controlValueDict = {'AlbumID': mbid} - newValueDict = {'Status': action} + newValueDict = {'Status': newaction} myDB.upsert("albums", newValueDict, controlValueDict) if action == 'Wanted': searcher.searchNZB(mbid, new=False) - raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) + if action == 'WantedNew': + searcher.searchNZB(mbid, new=True) + if ArtistID: + raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) + else: + raise cherrypy.HTTPRedirect("upcoming") markAlbums.exposed = True - def queueAlbum(self, AlbumID, ArtistID, new=False): + def queueAlbum(self, AlbumID, ArtistID=None, new=False, redirect=None): logger.info(u"Marking album: " + AlbumID + "as wanted...") myDB = db.DBConnection() controlValueDict = {'AlbumID': AlbumID} newValueDict = {'Status': 'Wanted'} myDB.upsert("albums", newValueDict, controlValueDict) searcher.searchNZB(AlbumID, new) - raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) + if ArtistID: + raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) + else: + raise cherrypy.HTTPRedirect(redirect) queueAlbum.exposed = True def unqueueAlbum(self, AlbumID, ArtistID): - logger.info(u"Marking album: " + AlbumID + "as skipped...") myDB = db.DBConnection() controlValueDict = {'AlbumID': AlbumID} newValueDict = {'Status': 'Skipped'} myDB.upsert("albums", newValueDict, controlValueDict) - raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) - unqueueAlbum.exposed = True def upcoming(self):
    Artist Album Name Release DateRelease TypeStatusType
    ${album['ArtistName']} ${album['AlbumTitle']} ${album['ReleaseDate']} ${album['Type']}${album['Status']}