diff options
author | ronie <ronie@kodi.tv> | 2019-03-04 02:07:02 +0100 |
---|---|---|
committer | ronie <ronie@kodi.tv> | 2020-06-22 23:29:04 +0200 |
commit | 8a805fd53fbcf5e87356a388ebdefdc638821ab1 (patch) | |
tree | b9bbbf6561a551425c22bfbe48be32f5f26794b8 /addons/metadata.generic.albums | |
parent | bf424ee5bd9d3cb65e5f15b36ff9361c79920045 (diff) |
add python album scraper
Diffstat (limited to 'addons/metadata.generic.albums')
-rw-r--r-- | addons/metadata.generic.albums/LICENSE.txt | 282 | ||||
-rw-r--r-- | addons/metadata.generic.albums/addon.xml | 20 | ||||
-rw-r--r-- | addons/metadata.generic.albums/changelog.txt | 31 | ||||
-rw-r--r-- | addons/metadata.generic.albums/default.py | 32 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/allmusic.py | 122 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/discogs.py | 59 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/fanarttv.py | 45 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/musicbrainz.py | 154 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/nfo.py | 8 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/scraper.py | 442 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/theaudiodb.py | 124 | ||||
-rw-r--r-- | addons/metadata.generic.albums/lib/utils.py | 24 | ||||
-rw-r--r-- | addons/metadata.generic.albums/resources/icon.png | bin | 0 -> 15700 bytes | |||
-rw-r--r-- | addons/metadata.generic.albums/resources/language/resource.language.en_gb/strings.po | 85 | ||||
-rw-r--r-- | addons/metadata.generic.albums/resources/settings.xml | 107 |
15 files changed, 1535 insertions, 0 deletions
diff --git a/addons/metadata.generic.albums/LICENSE.txt b/addons/metadata.generic.albums/LICENSE.txt new file mode 100644 index 0000000000..4f8e8eb30c --- /dev/null +++ b/addons/metadata.generic.albums/LICENSE.txt @@ -0,0 +1,282 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- diff --git a/addons/metadata.generic.albums/addon.xml b/addons/metadata.generic.albums/addon.xml new file mode 100644 index 0000000000..acdc7b4f16 --- /dev/null +++ b/addons/metadata.generic.albums/addon.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<addon id="metadata.generic.albums" name="Generic Album Scraper" version="1.0.7" provider-name="Team Kodi"> + <requires> + <import addon="xbmc.python" version="3.0.0"/> + <import addon="xbmc.metadata" version="2.1.0"/> + </requires> + <extension point="xbmc.metadata.scraper.albums" library="default.py"/> + <extension point="xbmc.addon.metadata"> + <summary lang="en_GB">Generic music scraper for albums</summary> + <description lang="en_GB">Searches for album information and artwork across multiple websites.</description> + <platform>all</platform> + <license>GPL-2.0-only</license> + <forum>https://forum.kodi.tv/showthread.php?tid=351570</forum> + <source>https://gitlab.com/ronie/metadata.generic.albums/</source> + <assets> + <icon>resources/icon.png</icon> + </assets> + <news>- first release</news> + </extension> +</addon> diff --git a/addons/metadata.generic.albums/changelog.txt b/addons/metadata.generic.albums/changelog.txt new file mode 100644 index 0000000000..9d67dccb9f --- /dev/null +++ b/addons/metadata.generic.albums/changelog.txt @@ -0,0 +1,31 @@ +v1.0.7 +- fix crash when album type is absent or empty in the api response +- filter inaccurate search results from discogs +- filter inaccurate albumdetails from allmusic +- filter blank allmusic album thumb +- consider both score and releasedate when selecting the top release from releasegroup + +v1.0.6 +- improve custom scoring +- add support for original release date +- fix release date from musicbrainz +- use releasegroup id to fetch coverartarchive artwork +- only use one release from each releasegroup +- provide detailed search results + +v1.0.5 +- don't set releasetype +- fix types from musicbrainz +- add release status + +v1.0.4 +- catch time-outs + +v1.0.3 +- replace beautifulsoup with regex + +v1.0.2 +- replace requests with urllib + +v1.0.1 +- release diff --git a/addons/metadata.generic.albums/default.py b/addons/metadata.generic.albums/default.py new file mode 100644 index 0000000000..85aeb90dff --- /dev/null +++ b/addons/metadata.generic.albums/default.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import sys +from urllib.parse import parse_qsl +from lib.scraper import Scraper + + +class Main: + def __init__(self): + action, key, artist, album, url, nfo, settings = self._parse_argv() + Scraper(action, key, artist, album, url, nfo, settings) + + def _parse_argv(self): + params = dict(parse_qsl(sys.argv[2].lstrip('?'))) + # actions: resolveid, find, getdetails, NfoUrl + action = params['action'] + # key: musicbrainz id + key = params.get('key', '') + # artist: artistname + artist = params.get('artist', '') + # album: albumtitle + album = params.get('title', '') + # url: provided by the scraper on previous run + url = params.get('url', '') + # nfo: musicbrainz url from .nfo file + nfo = params.get('nfo', '') + # path specific settings + settings = params.get('pathSettings', {}) + return action, key, artist, album, url, nfo, settings + + +if (__name__ == '__main__'): + Main() diff --git a/addons/metadata.generic.albums/lib/allmusic.py b/addons/metadata.generic.albums/lib/allmusic.py new file mode 100644 index 0000000000..ede10368c0 --- /dev/null +++ b/addons/metadata.generic.albums/lib/allmusic.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +import datetime +import difflib +import time +import re + +# not used for 'find', but needed for 'getdetails' +def allmusic_albumfind(data, artist, album): + data = data.decode('utf-8') + albums = [] + albumlist = re.findall('class="album">\s*(.*?)\s*</li', data, re.S) + for item in albumlist: + albumdata = {} + albumartist = re.search('class="artist">.*?>(.*?)</a', item, re.S) + if albumartist: + albumdata['artist'] = albumartist.group(1) + else: # classical album + continue + albumname = re.search('class="title">.*?>(.*?)</a', item, re.S) + if albumname: + albumdata['album'] = albumname.group(1) + else: # not likely to happen, but just in case + continue + # filter inaccurate results + artistmatch = difflib.SequenceMatcher(None, artist.decode('utf-8').lower(), albumdata['artist'].lower()).ratio() + albummatch = difflib.SequenceMatcher(None, album.decode('utf-8').lower(), albumdata['album'].lower()).ratio() + if artistmatch > 0.90 and albummatch > 0.90: + albumurl = re.search('class="title">\s*<a href="(.*?)"', item) + if albumurl: + albumdata['url'] = albumurl.group(1) + else: # not likely to happen, but just in case + continue + albumyear = re.search('class="year">\s*(.*?)\s*<', item, re.S) + if albumyear: + albumdata['year'] = albumyear.group(1) + else: + albumdata['year'] = '' + albumthumb = re.search('img src="(.*?)"', item) + if albumthumb: + albumdata['thumb'] = albumthumb.group(1) + else: + albumdata['thumb'] = '' + albums.append(albumdata) + return albums + +def allmusic_albumdetails(data): + data = data.decode('utf-8') + albumdata = {} + releasedata = re.search('class="release-date">.*?<span>(.*?)<', data, re.S) + if releasedata: + dateformat = releasedata.group(1) + if len(dateformat) > 4: + try: + # month day, year + albumdata['releasedate'] = datetime.datetime(*(time.strptime(dateformat, '%B %d, %Y')[0:3])).strftime('%Y-%m-%d') + except: + # month, year + albumdata['releasedate'] = datetime.datetime(*(time.strptime(dateformat, '%B, %Y')[0:3])).strftime('%Y-%m') + else: + # year + albumdata['releasedate'] = dateformat + yeardata = re.search('class="year".*?>\s*(.*?)\s*<', data) + if yeardata: + albumdata['year'] = yeardata.group(1) + genredata = re.search('class="genre">.*?">(.*?)<', data, re.S) + if genredata: + albumdata['genre'] = genredata.group(1) + styledata = re.search('class="styles">.*?div>\s*(.*?)\s*</div', data, re.S) + if styledata: + stylelist = re.findall('">(.*?)<', styledata.group(1)) + if stylelist: + albumdata['styles'] = ' / '.join(stylelist) + mooddata = re.search('class="moods">.*?div>\s*(.*?)\s*</div', data, re.S) + if mooddata: + moodlist = re.findall('">(.*?)<', mooddata.group(1)) + if moodlist: + albumdata['moods'] = ' / '.join(moodlist) + themedata = re.search('class="themes">.*?div>\s*(.*?)\s*</div', data, re.S) + if themedata: + themelist = re.findall('">(.*?)<', themedata.group(1)) + if themelist: + albumdata['themes'] = ' / '.join(themelist) + ratingdata = re.search('itemprop="ratingValue">\s*(.*?)\s*</div', data) + if ratingdata: + albumdata['rating'] = ratingdata.group(1) + albumdata['votes'] = '' + titledata = re.search('class="album-title".*?>\s*(.*?)\s*<', data, re.S) + if titledata: + albumdata['album'] = titledata.group(1) + labeldata = re.search('class="label-catalog".*?<.*?>(.*?)<', data, re.S) + if labeldata: + albumdata['label'] = labeldata.group(1) + artistdata = re.search('class="album-artist".*?<span.*?>\s*(.*?)\s*</span', data, re.S) + if artistdata: + artistlist = re.findall('">(.*?)<', artistdata.group(1)) + artists = [] + for item in artistlist: + artistinfo = {} + artistinfo['artist'] = item + artists.append(artistinfo) + if artists: + albumdata['artist'] = artists + albumdata['artist_description'] = ' / '.join(artistlist) + thumbsdata = re.search('class="album-contain".*?src="(.*?)"', data, re.S) + if thumbsdata: + thumbs = [] + thumbdata = {} + thumb = thumbsdata.group(1).rstrip('?partner=allrovi.com') + # ignore internal blank thumb + if thumb.startswith('http'): + # 0=largest / 1=75 / 2=150 / 3=250 / 4=400 / 5=500 / 6=1080 + if thumb.endswith('f=5'): + thumbdata['image'] = thumb.replace('f=5', 'f=0') + thumbdata['preview'] = thumb.replace('f=5', 'f=2') + else: + thumbdata['image'] = thumb + thumbdata['preview'] = thumb + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + albumdata['thumb'] = thumbs + return albumdata diff --git a/addons/metadata.generic.albums/lib/discogs.py b/addons/metadata.generic.albums/lib/discogs.py new file mode 100644 index 0000000000..09730ef0fc --- /dev/null +++ b/addons/metadata.generic.albums/lib/discogs.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +import difflib + +def discogs_albumfind(data, artist, album): + albums = [] + masters = [] + # sort results by lowest release id (first version of a release) + releases = sorted(data.get('results',[]), key=lambda k: k['id']) + for item in releases: + masterid = item['master_id'] + # we are not interested in multiple versions that belong to the same master release + if masterid not in masters: + masters.append(masterid) + albumdata = {} + albumdata['artist'] = item['title'].split(' - ',1)[0] + albumdata['album'] = item['title'].split(' - ',1)[1] + albumdata['artist_description'] = item['title'].split(' - ',1)[0] + albumdata['year'] = str(item.get('year', '')) + albumdata['label'] = item['label'][0] + albumdata['thumb'] = item['thumb'] + albumdata['dcalbumid'] = item['id'] + # discogs does not provide relevance, use our own + artistmatch = difflib.SequenceMatcher(None, artist.lower(), albumdata['artist'].lower()).ratio() + albummatch = difflib.SequenceMatcher(None, album.lower(), albumdata['album'].lower()).ratio() + artistscore = round(artistmatch, 2) + albumscore = round(albummatch, 2) + score = round(((artistscore + albumscore) / 2), 2) + albumdata['relevance'] = str(score) + albums.append(albumdata) + return albums + +def discogs_albumdetails(data): + albumdata = {} + albumdata['album'] = data['title'] + if 'styles' in data: + albumdata['styles'] = ' / '.join(data['styles']) + albumdata['genres'] = ' / '.join(data['genres']) + albumdata['year'] = str(data['year']) + albumdata['label'] = data['labels'][0]['name'] + artists = [] + for artist in data['artists']: + artistdata = {} + artistdata['artist'] = artist['name'] + artists.append(artistdata) + albumdata['artist'] = artists + albumdata['artist_description'] = data['artists_sort'] + albumdata['rating'] = str(int((float(data['community']['rating']['average']) * 2) + 0.5)) + albumdata['votes'] = str(data['community']['rating']['count']) + if 'images' in data: + thumbs = [] + for thumb in data['images']: + thumbdata = {} + thumbdata['image'] = thumb['uri'] + thumbdata['preview'] = thumb['uri150'] + # not accurate: discogs can provide any art type, there is no indication if it is an album front cover (thumb) + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + albumdata['thumb'] = thumbs + return albumdata diff --git a/addons/metadata.generic.albums/lib/fanarttv.py b/addons/metadata.generic.albums/lib/fanarttv.py new file mode 100644 index 0000000000..1724945078 --- /dev/null +++ b/addons/metadata.generic.albums/lib/fanarttv.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +def fanarttv_albumart(data): + if 'albums' in data: + albumdata = {} + thumbs = [] + extras = [] + discs = {} + for mbid, art in data['albums'].items(): + if 'albumcover' in art: + for thumb in art['albumcover']: + thumbdata = {} + thumbdata['image'] = thumb['url'] + thumbdata['preview'] = thumb['url'].replace('/fanart/', '/preview/') + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + if 'cdart' in art: + albumdata['discart'] = art['cdart'][0]['url'] + for cdart in art['cdart']: + extradata = {} + extradata['image'] = cdart['url'] + extradata['preview'] = cdart['url'].replace('/fanart/', '/preview/') + extradata['aspect'] = 'discart' + extras.append(extradata) + # support for multi-disc albums + multidata = {} + num = cdart['disc'] + multidata['image'] = cdart['url'] + multidata['preview'] = cdart['url'].replace('/fanart/', '/preview/') + multidata['aspect'] = 'discart%s' % num + if not num in discs: + discs[num] = [multidata] + else: + discs[num].append(multidata) + if thumbs: + albumdata['thumb'] = thumbs + # only return for multi-discs, not single discs + if len(discs) > 1: + albumdata['multidiscart'] = discs + for k, v in discs.items(): + for item in v: + extras.append(item) + if extras: + albumdata['extras'] = extras + return albumdata diff --git a/addons/metadata.generic.albums/lib/musicbrainz.py b/addons/metadata.generic.albums/lib/musicbrainz.py new file mode 100644 index 0000000000..0b7c9a36f6 --- /dev/null +++ b/addons/metadata.generic.albums/lib/musicbrainz.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +def musicbrainz_albumfind(data, artist, album): + albums = [] + # count how often each releasegroup occurs in the release results + # keep track of the release with the highest score and earliest releasedate in each releasegroup + releasegroups = {} + for item in data.get('releases'): + mbid = item['id'] + score = item.get('score', 0) + releasegroup = item['release-group']['id'] + if 'date' in item and item['date']: + date = item['date'].replace('-','') + if len(date) == 4: + date = date + '9999' + else: + date = '99999999' + if releasegroup in releasegroups: + count = releasegroups[releasegroup][0] + 1 + topmbid = releasegroups[releasegroup][1] + topdate = releasegroups[releasegroup][2] + topscore = releasegroups[releasegroup][3] + if date < topdate and score >= topscore: + topdate = date + topmbid = mbid + releasegroups[releasegroup] = [count, topmbid, topdate, topscore] + else: + releasegroups[releasegroup] = [1, mbid, date, score] + if releasegroups: + # get the highest releasegroup count + maxcount = max(releasegroups.values())[0] + # get the releasegroup(s) that match this highest value + topgroups = [k for k, v in releasegroups.items() if v[0] == maxcount] + for item in data.get('releases'): + # only use the 'top' release from each releasegroup + if item['id'] != releasegroups[item['release-group']['id']][1]: + continue + albumdata = {} + if item.get('artist-credit'): + artists = [] + artistdisp = "" + for artist in item['artist-credit']: + artistdata = {} + artistdata['artist'] = artist['artist']['name'] + artistdata['mbartistid'] = artist['artist']['id'] + artistdata['artistsort'] = artist['artist']['sort-name'] + artistdisp = artistdisp + artist['artist']['name'] + artistdisp = artistdisp + artist.get('joinphrase', '') + artists.append(artistdata) + albumdata['artist'] = artists + albumdata['artist_description'] = artistdisp + if item.get('label-info','') and item['label-info'][0].get('label','') and item['label-info'][0]['label'].get('name',''): + albumdata['label'] = item['label-info'][0]['label']['name'] + albumdata['album'] = item['title'] + if item.get('date',''): + albumdata['year'] = item['date'][:4] + albumdata['thumb'] = 'https://coverartarchive.org/release-group/%s/front-250' % item['release-group']['id'] + if item.get('label-info','') and item['label-info'][0].get('label','') and item['label-info'][0]['label'].get('name',''): + albumdata['label'] = item['label-info'][0]['label']['name'] + if item.get('status',''): + albumdata['releasestatus'] = item['status'] + albumdata['type'] = item['release-group'].get('primary-type') + albumdata['mbalbumid'] = item['id'] + albumdata['mbreleasegroupid'] = item['release-group']['id'] + if item.get('score'): + releasescore = item['score'] / 100.0 + # if the release is in the releasegroup with most releases, it is considered the most accurate one + # (this also helps with prefering official releases over bootlegs, assuming there are more variations of an official release than of a bootleg) + if item['release-group']['id'] not in topgroups: + releasescore -= 0.001 + # if the release is an album, prefer it over singles/ep's + # (this needs to be the double of the above, as me might have just given the album a lesser score if the single happened to be in the topgroup) + if item['release-group'].get('primary-type') != 'Album': + releasescore -= 0.002 + albumdata['relevance'] = str(releasescore) + albums.append(albumdata) + return albums + +def musicbrainz_albumdetails(data): + albumdata = {} + albumdata['album'] = data['title'] + albumdata['mbalbumid'] = data['id'] + if data.get('release-group',''): + albumdata['mbreleasegroupid'] = data['release-group']['id'] + if data['release-group']['rating'] and data['release-group']['rating']['value']: + albumdata['rating'] = str(int((float(data['release-group']['rating']['value']) * 2) + 0.5)) + albumdata['votes'] = str(data['release-group']['rating']['votes-count']) + if data['release-group'].get('primary-type'): + albumtypes = [data['release-group']['primary-type']] + data['release-group']['secondary-types'] + albumdata['type'] = ' / '.join(albumtypes) + if 'Compilation' in albumtypes: + albumdata['compilation'] = 'true' + if data['release-group'].get('first-release-date',''): + albumdata['originaldate'] = data['release-group']['first-release-date'] + if data.get('release-events',''): + albumdata['year'] = data['release-events'][0]['date'][:4] + albumdata['releasedate'] = data['release-events'][0]['date'] + if data.get('label-info','') and data['label-info'][0].get('label','') and data['label-info'][0]['label'].get('name',''): + albumdata['label'] = data['label-info'][0]['label']['name'] + if data.get('status',''): + albumdata['releasestatus'] = data['status'] + if data.get('artist-credit'): + artists = [] + artistdisp = '' + for artist in data['artist-credit']: + artistdata = {} + artistdata['artist'] = artist['name'] + artistdata['mbartistid'] = artist['artist']['id'] + artistdata['artistsort'] = artist['artist']['sort-name'] + artistdisp = artistdisp + artist['name'] + artistdisp = artistdisp + artist.get('joinphrase', '') + artists.append(artistdata) + albumdata['artist'] = artists + albumdata['artist_description'] = artistdisp + return albumdata + +def musicbrainz_albumart(data): + albumdata = {} + thumbs = [] + extras = [] + for item in data['images']: + if 'Front' in item['types']: + thumbdata = {} + thumbdata['image'] = item['image'] + thumbdata['preview'] = item['thumbnails']['small'] + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + if 'Back' in item['types']: + albumdata['back'] = item['image'] + backdata = {} + backdata['image'] = item['image'] + backdata['preview'] = item['thumbnails']['small'] + backdata['aspect'] = 'back' + extras.append(backdata) + if 'Medium' in item['types']: + albumdata['discart'] = item['image'] + discartdata = {} + discartdata['image'] = item['image'] + discartdata['preview'] = item['thumbnails']['small'] + discartdata['aspect'] = 'discart' + extras.append(discartdata) + # exculde spine+back images + if 'Spine' in item['types'] and len(item['types']) == 1: + albumdata['spine'] = item['image'] + spinedata = {} + spinedata['image'] = item['image'] + spinedata['preview'] = item['thumbnails']['small'] + spinedata['aspect'] = 'spine' + extras.append(spinedata) + if thumbs: + albumdata['thumb'] = thumbs + if extras: + albumdata['extras'] = extras + return albumdata diff --git a/addons/metadata.generic.albums/lib/nfo.py b/addons/metadata.generic.albums/lib/nfo.py new file mode 100644 index 0000000000..ef996ce52b --- /dev/null +++ b/addons/metadata.generic.albums/lib/nfo.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +import re + +def nfo_geturl(data): + result = re.search('https://musicbrainz.org/(ws/2/)?release/([0-9a-z\-]*)', data) + if result: + return result.group(2) diff --git a/addons/metadata.generic.albums/lib/scraper.py b/addons/metadata.generic.albums/lib/scraper.py new file mode 100644 index 0000000000..7a05a11c58 --- /dev/null +++ b/addons/metadata.generic.albums/lib/scraper.py @@ -0,0 +1,442 @@ +# -*- coding: utf-8 -*- + +import json +import socket +import sys +import time +import urllib.parse +import urllib.request +import _strptime # https://bugs.python.org/issue7980 +from threading import Thread +from urllib.error import HTTPError, URLError +from socket import timeout +import xbmc +import xbmcgui +import xbmcplugin +import xbmcaddon +from .theaudiodb import theaudiodb_albumdetails +from .musicbrainz import musicbrainz_albumfind +from .musicbrainz import musicbrainz_albumdetails +from .musicbrainz import musicbrainz_albumart +from .discogs import discogs_albumfind +from .discogs import discogs_albumdetails +from .allmusic import allmusic_albumfind +from .allmusic import allmusic_albumdetails +from .nfo import nfo_geturl +from .fanarttv import fanarttv_albumart +from .utils import * + +ADDONID = xbmcaddon.Addon().getAddonInfo('id') +ADDONNAME = xbmcaddon.Addon().getAddonInfo('name') +ADDONVERSION = xbmcaddon.Addon().getAddonInfo('version') + + +def log(txt): + message = '%s: %s' % (ADDONID, txt) + xbmc.log(msg=message, level=xbmc.LOGDEBUG) + +def get_data(url, jsonformat): + try: + headers = {} + headers['User-Agent'] = '%s/%s ( http://kodi.tv )' % (ADDONNAME, ADDONVERSION) + req = urllib.request.Request(url, headers=headers) + resp = urllib.request.urlopen(req, timeout=5) + respdata = resp.read() + except URLError as e: + log('URLError: %s - %s' % (e.reason, url)) + return + except HTTPError as e: + log('HTTPError: %s - %s' % (e.reason, url)) + return + except socket.timeout as e: + log('socket: %s - %s' % (e, url)) + return + if resp.getcode() == 503: + log('exceeding musicbrainz api limit') + return + elif resp.getcode() == 429: + log('exceeding discogs api limit') + return + if jsonformat: + respdata = json.loads(respdata) + return respdata + + +class Scraper(): + def __init__(self, action, key, artist, album, url, nfo, settings): + # get start time in milliseconds + self.start = int(round(time.time() * 1000)) + # parse path settings + self.parse_settings(settings) + # return a dummy result, this is just for backward compitability with xml based scrapers https://github.com/xbmc/xbmc/pull/11632 + if action == 'resolveid': + result = self.resolve_mbid(key) + if result: + self.return_resolved(result) + # search for artist name / album title matches + elif action == 'find': + # both musicbrainz and discogs allow 1 api per second. this query requires 1 musicbrainz api call and optionally 1 discogs api call + RATELIMIT = 1000 + # try musicbrainz first + result = self.find_album(artist, album, 'musicbrainz') + if result: + self.return_search(result) + # fallback to discogs + else: + result = self.find_album(artist, album, 'discogs') + if result: + self.return_search(result) + # return info using artistname / albumtitle / id's + elif action == 'getdetails': + details = {} + url = json.loads(url) + artist = url['artist'].encode('utf-8') + album = url['album'].encode('utf-8') + mbid = url.get('mbalbumid', '') + dcid = url.get('dcalbumid', '') + mbreleasegroupid = url.get('mbreleasegroupid', '') + threads = [] + # we have a musicbrainz album id, but no musicbrainz releasegroupid + if mbid and not mbreleasegroupid: + # musicbrainz allows 1 api per second. + RATELIMIT = 1000 + for item in [[mbid, 'musicbrainz']]: + thread = Thread(target = self.get_details, args = (item[0], item[1], details)) + threads.append(thread) + thread.start() + # wait for musicbrainz to finish + threads[0].join() + # check if we have a result: + if 'musicbrainz' in details: + artist = details['musicbrainz']['artist_description'].encode('utf-8') + album = details['musicbrainz']['album'].encode('utf-8') + mbreleasegroupid = details['musicbrainz']['mbreleasegroupid'] + scrapers = [[mbreleasegroupid, 'theaudiodb'], [mbreleasegroupid, 'fanarttv'], [mbreleasegroupid, 'coverarchive'], [[artist, album], 'allmusic']] + if self.usediscogs == 1: + scrapers.append([[artist, album, dcid], 'discogs']) + # discogs allows 1 api per second. this query requires 2 discogs api calls + RATELIMIT = 2000 + for item in scrapers: + thread = Thread(target = self.get_details, args = (item[0], item[1], details)) + threads.append(thread) + thread.start() + # we have a discogs id and artistname and albumtitle + elif dcid: + # discogs allows 1 api per second. this query requires 1 discogs api call + RATELIMIT = 1000 + for item in [[[artist, album, dcid], 'discogs'], [[artist, album], 'allmusic']]: + thread = Thread(target = self.get_details, args = (item[0], item[1], details)) + threads.append(thread) + thread.start() + # we have musicbrainz album id, musicbrainz releasegroupid, artistname and albumtitle + else: + # musicbrainz allows 1 api per second. + RATELIMIT = 1000 + scrapers = [[mbid, 'musicbrainz'], [mbreleasegroupid, 'theaudiodb'], [mbreleasegroupid, 'fanarttv'], [mbreleasegroupid, 'coverarchive'], [[artist, album], 'allmusic']] + if self.usediscogs == 1: + scrapers.append([[artist, album, dcid], 'discogs']) + # discogs allows 1 api per second. this query requires 2 discogs api calls + RATELIMIT = 2000 + for item in scrapers: + thread = Thread(target = self.get_details, args = (item[0], item[1], details)) + threads.append(thread) + thread.start() + for thread in threads: + thread.join() + result = self.compile_results(details) + if result: + self.return_details(result) + # extract the mbid from the provided musicbrainz url + elif action == 'NfoUrl': + mbid = nfo_geturl(nfo) + if mbid: + # create a dummy item + result = self.resolve_mbid(mbid) + if result: + self.return_nfourl(result) + # get end time in milliseconds + self.end = int(round(time.time() * 1000)) + # handle musicbrainz and discogs ratelimit + if action == 'find' or action == 'getdetails': + if self.end - self.start < RATELIMIT: + # wait max 2 seconds + diff = RATELIMIT - (self.end - self.start) + xbmc.sleep(diff) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + + def parse_settings(self, data): + settings = json.loads(data) + # note: path settings are taken from the db, they may not reflect the current settings.xml file + self.genre = settings['genre'] + self.lang = settings['lang'] + self.mood = settings['mood'] + self.rating = settings['rating'] + self.style = settings['style'] + self.theme = settings['theme'] + self.usediscogs = settings['usediscogs'] + + def resolve_mbid(self, mbid): + # create dummy result + item = {} + item['artist_description'] = '' + item['album'] = '' + item['mbalbumid'] = mbid + item['mbreleasegroupid'] = '' + return item + + def find_album(self, artist, album, site): + json = True + # musicbrainz + if site == 'musicbrainz': + url = MUSICBRAINZURL % (MUSICBRAINZSEARCH % (urllib.parse.quote_plus(album), urllib.parse.quote_plus(artist), urllib.parse.quote_plus(artist))) + scraper = musicbrainz_albumfind + # discogs + elif site == 'discogs': + url = DISCOGSURL % (DISCOGSSEARCH % (urllib.parse.quote_plus(album), urllib.parse.quote_plus(artist), DISCOGSKEY , DISCOGSSECRET)) + scraper = discogs_albumfind + # allmusic + elif site == 'allmusic': + url = ALLMUSICURL % (ALLMUSICSEARCH % (urllib.parse.quote_plus(artist), urllib.parse.quote_plus(album))) + scraper = allmusic_albumfind + json = False + result = get_data(url, json) + if not result: + return + albumresults = scraper(result, artist, album) + return albumresults + + def get_details(self, param, site, details): + json = True + # theaudiodb + if site == 'theaudiodb': + url = AUDIODBURL % (AUDIODBKEY, AUDIODBDETAILS % param) + albumscraper = theaudiodb_albumdetails + # musicbrainz + elif site == 'musicbrainz': + url = MUSICBRAINZURL % (MUSICBRAINZDETAILS % param) + albumscraper = musicbrainz_albumdetails + # fanarttv + elif site == 'fanarttv': + url = FANARTVURL % (param, FANARTVKEY) + albumscraper = fanarttv_albumart + # coverarchive + elif site == 'coverarchive': + url = MUSICBRAINZART % (param) + albumscraper = musicbrainz_albumart + # discogs + elif site == 'discogs': + dcalbumid = param[2] + if not dcalbumid: + # search + found = self.find_album(param[0], param[1], 'discogs') + if found: + # get details + dcalbumid = found[0]['dcalbumid'] + else: + return + url = DISCOGSURL % (DISCOGSDETAILS % (dcalbumid, DISCOGSKEY, DISCOGSSECRET)) + albumscraper = discogs_albumdetails + # allmusic + elif site == 'allmusic': + # search + found = self.find_album(param[0], param[1], 'allmusic') + if found: + # get details + url = ALLMUSICDETAILS % found[0]['url'] + albumscraper = allmusic_albumdetails + json = False + else: + return + result = get_data(url, json) + if not result: + return + albumresults = albumscraper(result) + if not albumresults: + return + details[site] = albumresults + return details + + def compile_results(self, details): + result = {} + thumbs = [] + extras = [] + # merge metadata results, start with the least accurate sources + if 'discogs' in details: + for k, v in details['discogs'].items(): + result[k] = v + if k == 'thumb': + thumbs.append(v) + if 'allmusic' in details: + for k, v in details['allmusic'].items(): + result[k] = v + if k == 'thumb': + thumbs.append(v) + if 'theaudiodb' in details: + for k, v in details['theaudiodb'].items(): + result[k] = v + if k == 'thumb': + thumbs.append(v) + if k == 'extras': + extras.append(v) + if 'musicbrainz' in details: + for k, v in details['musicbrainz'].items(): + result[k] = v + if 'coverarchive' in details: + for k, v in details['coverarchive'].items(): + result[k] = v + if k == 'thumb': + thumbs.append(v) + if k == 'extras': + extras.append(v) + # prefer artwork from fanarttv + if 'fanarttv' in details: + for k, v in details['fanarttv'].items(): + result[k] = v + if k == 'thumb': + thumbs.append(v) + if k == 'extras': + extras.append(v) + # use musicbrainz artist list as they provide mbid's, these can be passed to the artist scraper + if 'musicbrainz' in details: + result['artist'] = details['musicbrainz']['artist'] + # provide artwork from all scrapers for getthumb option + if result: + # thumb list from most accurate sources first + thumbs.reverse() + thumbnails = [] + for thumblist in thumbs: + for item in thumblist: + thumbnails.append(item) + # the order for extra art does not matter + extraart = [] + for extralist in extras: + for item in extralist: + extraart.append(item) + # add the extra art to the end of the thumb list + thumbnails.extend(extraart) + result['thumb'] = thumbnails + data = self.user_prefs(details, result) + return data + + def user_prefs(self, details, result): + # user preferences + lang = 'description' + self.lang + if 'theaudiodb' in details: + if lang in details['theaudiodb']: + result['description'] = details['theaudiodb'][lang] + elif 'descriptionEN' in details['theaudiodb']: + result['description'] = details['theaudiodb']['descriptionEN'] + if (self.genre in details) and ('genre' in details[self.genre]): + result['genre'] = details[self.genre]['genre'] + if (self.style in details) and ('styles' in details[self.style]): + result['styles'] = details[self.style]['styles'] + if (self.mood in details) and ('moods' in details[self.mood]): + result['moods'] = details[self.mood]['moods'] + if (self.theme in details) and ('themes' in details[self.theme]): + result['themes'] = details[self.theme]['themes'] + if (self.rating in details) and ('rating' in details[self.rating]): + result['rating'] = details[self.rating]['rating'] + result['votes'] = details[self.rating]['votes'] + return result + + def return_search(self, data): + for count, item in enumerate(data): + listitem = xbmcgui.ListItem(item['album'], offscreen=True) + listitem.setArt({'thumb': item['thumb']}) + listitem.setProperty('album.artist', item['artist_description']) + listitem.setProperty('album.year', item.get('year','')) + listitem.setProperty('album.type', item.get('type','')) + listitem.setProperty('album.releasestatus', item.get('releasestatus','')) + listitem.setProperty('album.label', item.get('label','')) + listitem.setProperty('relevance', item['relevance']) + url = {'artist':item['artist_description'], 'album':item['album']} + if 'mbalbumid' in item: + url['mbalbumid'] = item['mbalbumid'] + if 'mbreleasegroupid' in item: + url['mbreleasegroupid'] = item['mbreleasegroupid'] + if 'dcalbumid' in item: + url['dcalbumid'] = item['dcalbumid'] + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=json.dumps(url), listitem=listitem, isFolder=True) + + def return_nfourl(self, item): + url = {'artist':item['artist_description'], 'album':item['album'], 'mbalbumid':item['mbalbumid'], 'mbreleasegroupid':item['mbreleasegroupid']} + listitem = xbmcgui.ListItem(offscreen=True) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=json.dumps(url), listitem=listitem, isFolder=True) + + def return_resolved(self, item): + url = {'artist':item['artist_description'], 'album':item['album'], 'mbalbumid':item['mbalbumid'], 'mbreleasegroupid':item['mbreleasegroupid']} + listitem = xbmcgui.ListItem(path=json.dumps(url), offscreen=True) + xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=listitem) + + def return_details(self, item): + if not 'album' in item: + return + listitem = xbmcgui.ListItem(item['album'], offscreen=True) + if 'mbalbumid' in item: + listitem.setProperty('album.musicbrainzid', item['mbalbumid']) + listitem.setProperty('album.releaseid', item['mbalbumid']) + if 'mbreleasegroupid' in item: + listitem.setProperty('album.releasegroupid', item['mbreleasegroupid']) + if 'scrapedmbid' in item: + listitem.setProperty('album.scrapedmbid', item['scrapedmbid']) + if 'artist' in item: + listitem.setProperty('album.artists', str(len(item['artist']))) + for count, artist in enumerate(item['artist']): + listitem.setProperty('album.artist%i.name' % (count + 1), artist['artist']) + listitem.setProperty('album.artist%i.musicbrainzid' % (count + 1), artist.get('mbartistid', '')) + listitem.setProperty('album.artist%i.sortname' % (count + 1), artist.get('artistsort', '')) + if 'genre' in item: + listitem.setProperty('album.genre', item['genre']) + if 'styles' in item: + listitem.setProperty('album.styles', item['styles']) + if 'moods' in item: + listitem.setProperty('album.moods', item['moods']) + if 'themes' in item: + listitem.setProperty('album.themes', item['themes']) + if 'description' in item: + listitem.setProperty('album.review', item['description']) + if 'releasedate' in item: + listitem.setProperty('album.releasedate', item['releasedate']) + if 'originaldate' in item: + listitem.setProperty('album.originaldate', item['originaldate']) + if 'releasestatus' in item: + listitem.setProperty('album.releasestatus', item['releasestatus']) + if 'artist_description' in item: + listitem.setProperty('album.artist_description', item['artist_description']) + if 'label' in item: + listitem.setProperty('album.label', item['label']) + if 'type' in item: + listitem.setProperty('album.type', item['type']) + if 'compilation' in item: + listitem.setProperty('album.compilation', item['compilation']) + if 'year' in item: + listitem.setProperty('album.year', item['year']) + if 'rating' in item: + listitem.setProperty('album.rating', item['rating']) + if 'votes' in item: + listitem.setProperty('album.votes', item['votes']) + art = {} + if 'discart' in item: + art['discart'] = item['discart'] + if 'multidiscart' in item: + for k, v in item['multidiscart'].items(): + discart = 'discart%s' % k + art[discart] = v[0]['image'] + if 'back' in item: + art['back'] = item['back'] + if 'spine' in item: + art['spine'] = item['spine'] + if '3dcase' in item: + art['3dcase'] = item['3dcase'] + if '3dflat' in item: + art['3dflat'] = item['3dflat'] + if '3dface' in item: + art['3dface'] = item['3dface'] + listitem.setArt(art) + if 'thumb' in item: + listitem.setProperty('album.thumbs', str(len(item['thumb']))) + for count, thumb in enumerate(item['thumb']): + listitem.setProperty('album.thumb%i.url' % (count + 1), thumb['image']) + listitem.setProperty('album.thumb%i.aspect' % (count + 1), thumb['aspect']) + listitem.setProperty('album.thumb%i.preview' % (count + 1), thumb['preview']) + xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=listitem) diff --git a/addons/metadata.generic.albums/lib/theaudiodb.py b/addons/metadata.generic.albums/lib/theaudiodb.py new file mode 100644 index 0000000000..196e195c9a --- /dev/null +++ b/addons/metadata.generic.albums/lib/theaudiodb.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +def theaudiodb_albumdetails(data): + if data.get('album'): + item = data['album'][0] + albumdata = {} + albumdata['album'] = item['strAlbum'] + if item.get('intYearReleased',''): + albumdata['year'] = item['intYearReleased'] + if item.get('strStyle',''): + albumdata['styles'] = item['strStyle'] + if item.get('strGenre',''): + albumdata['genre'] = item['strGenre'] + if item.get('strLabel',''): + albumdata['label'] = item['strLabel'] + if item.get('strReleaseFormat',''): + albumdata['type'] = item['strReleaseFormat'] + if item.get('intScore',''): + albumdata['rating'] = str(int(float(item['intScore']) + 0.5)) + if item.get('intScoreVotes',''): + albumdata['votes'] = item['intScoreVotes'] + if item.get('strMood',''): + albumdata['moods'] = item['strMood'] + if item.get('strTheme',''): + albumdata['themes'] = item['strTheme'] + if item.get('strMusicBrainzID',''): + albumdata['mbreleasegroupid'] = item['strMusicBrainzID'] + # api inconsistent + if item.get('strDescription',''): + albumdata['descriptionEN'] = item['strDescription'] + elif item.get('strDescriptionEN',''): + albumdata['descriptionEN'] = item['strDescriptionEN'] + if item.get('strDescriptionDE',''): + albumdata['descriptionDE'] = item['strDescriptionDE'] + if item.get('strDescriptionFR',''): + albumdata['descriptionFR'] = item['strDescriptionFR'] + if item.get('strDescriptionCN',''): + albumdata['descriptionCN'] = item['strDescriptionCN'] + if item.get('strDescriptionIT',''): + albumdata['descriptionIT'] = item['strDescriptionIT'] + if item.get('strDescriptionJP',''): + albumdata['descriptionJP'] = item['strDescriptionJP'] + if item.get('strDescriptionRU',''): + albumdata['descriptionRU'] = item['strDescriptionRU'] + if item.get('strDescriptionES',''): + albumdata['descriptionES'] = item['strDescriptionES'] + if item.get('strDescriptionPT',''): + albumdata['descriptionPT'] = item['strDescriptionPT'] + if item.get('strDescriptionSE',''): + albumdata['descriptionSE'] = item['strDescriptionSE'] + if item.get('strDescriptionNL',''): + albumdata['descriptionNL'] = item['strDescriptionNL'] + if item.get('strDescriptionHU',''): + albumdata['descriptionHU'] = item['strDescriptionHU'] + if item.get('strDescriptionNO',''): + albumdata['descriptionNO'] = item['strDescriptionNO'] + if item.get('strDescriptionIL',''): + albumdata['descriptionIL'] = item['strDescriptionIL'] + if item.get('strDescriptionPL',''): + albumdata['descriptionPL'] = item['strDescriptionPL'] + if item.get('strArtist',''): + albumdata['artist_description'] = item['strArtist'] + artists = [] + artistdata = {} + artistdata['artist'] = item['strArtist'] + if item.get('strMusicBrainzArtistID',''): + artistdata['mbartistid'] = item['strMusicBrainzArtistID'] + artists.append(artistdata) + albumdata['artist'] = artists + thumbs = [] + extras = [] + if item.get('strAlbumThumb',''): + thumbdata = {} + thumbdata['image'] = item['strAlbumThumb'] + thumbdata['preview'] = item['strAlbumThumb'] + '/preview' + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + if item.get('strAlbumThumbBack',''): + albumdata['back'] = item['strAlbumThumbBack'] + extradata = {} + extradata['image'] = item['strAlbumThumbBack'] + extradata['preview'] = item['strAlbumThumbBack'] + '/preview' + extradata['aspect'] = 'back' + extras.append(extradata) + if item.get('strAlbumSpine',''): + albumdata['spine'] = item['strAlbumSpine'] + extradata = {} + extradata['image'] = item['strAlbumSpine'] + extradata['preview'] = item['strAlbumSpine'] + '/preview' + extradata['aspect'] = 'spine' + extras.append(extradata) + if item.get('strAlbumCDart',''): + albumdata['discart'] = item['strAlbumCDart'] + extradata = {} + extradata['image'] = item['strAlbumCDart'] + extradata['preview'] = item['strAlbumCDart'] + '/preview' + extradata['aspect'] = 'discart' + extras.append(extradata) + if item.get('strAlbum3DCase',''): + albumdata['3dcase'] = item['strAlbum3DCase'] + extradata = {} + extradata['image'] = item['strAlbum3DCase'] + extradata['preview'] = item['strAlbum3DCase'] + '/preview' + extradata['aspect'] = '3dcase' + extras.append(extradata) + if item.get('strAlbum3DFlat',''): + albumdata['3dflat'] = item['strAlbum3DFlat'] + extradata = {} + extradata['image'] = item['strAlbum3DFlat'] + extradata['preview'] = item['strAlbum3DFlat'] + '/preview' + extradata['aspect'] = '3dflat' + extras.append(extradata) + if item.get('strAlbum3DFace',''): + albumdata['3dface'] = item['strAlbum3DFace'] + extradata = {} + extradata['image'] = item['strAlbum3DFace'] + extradata['preview'] = item['strAlbum3DFace'] + '/preview' + extradata['aspect'] = '3dface' + extras.append(extradata) + if thumbs: + albumdata['thumb'] = thumbs + if extras: + albumdata['extras'] = extras + return albumdata diff --git a/addons/metadata.generic.albums/lib/utils.py b/addons/metadata.generic.albums/lib/utils.py new file mode 100644 index 0000000000..e0ef3083c6 --- /dev/null +++ b/addons/metadata.generic.albums/lib/utils.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +AUDIODBKEY = '58424d43204d6564696120' +AUDIODBURL = 'https://www.theaudiodb.com/api/v1/json/%s/%s' +AUDIODBSEARCH = 'searchalbum.php?s=%s&a=%s' +AUDIODBDETAILS = 'album-mb.php?i=%s' + +MUSICBRAINZURL = 'https://musicbrainz.org/ws/2/release/%s' +MUSICBRAINZSEARCH = '?query=release:"%s"%%20AND%%20(artistname:"%s"%%20OR%%20artist:"%s")&fmt=json' +MUSICBRAINZDETAILS = '%s?inc=recordings+release-groups+artists+labels+ratings&fmt=json' +MUSICBRAINZART = 'https://coverartarchive.org/release-group/%s' + +DISCOGSKEY = 'zACPgktOmNegwbwKWMaC' +DISCOGSSECRET = 'wGuSOeMtfdkQxtERKQKPquyBwExSHdQq' +DISCOGSURL = 'https://api.discogs.com/%s' +DISCOGSSEARCH = 'database/search?release_title=%s&type=release&artist=%s&page=1&per_page=100&key=%s&secret=%s' +DISCOGSDETAILS = 'releases/%i?key=%s&secret=%s' + +ALLMUSICURL = 'https://www.allmusic.com/%s' +ALLMUSICSEARCH = 'search/albums/%s+%s' +ALLMUSICDETAILS = '%s/releases' + +FANARTVKEY = 'ed4b784f97227358b31ca4dd966a04f1' +FANARTVURL = 'https://webservice.fanart.tv/v3/music/albums/%s?api_key=%s' diff --git a/addons/metadata.generic.albums/resources/icon.png b/addons/metadata.generic.albums/resources/icon.png Binary files differnew file mode 100644 index 0000000000..b6748b3f54 --- /dev/null +++ b/addons/metadata.generic.albums/resources/icon.png diff --git a/addons/metadata.generic.albums/resources/language/resource.language.en_gb/strings.po b/addons/metadata.generic.albums/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..f0e777ee6b --- /dev/null +++ b/addons/metadata.generic.albums/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,85 @@ +# Kodi Media Center language file +# Addon Name: Generic Album Scraper +# Addon id: metadata.generic.albums +# Addon Provider: Team Kodi +msgid "" +msgstr "" +"Project-Id-Version: KODI Main\n" +"Report-Msgid-Bugs-To: https://github.com/xbmc/xbmc/issues\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Kodi Translation Team\n" +"Language-Team: English (United Kingdom) (http://www.transifex.com/projects/p/kodi-main/language/en_GB/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#30000" +msgid "Preferences" +msgstr "" + +msgctxt "#30001" +msgid "Prefered language for album review" +msgstr "" + +msgctxt "#30002" +msgid "Prefer genres from" +msgstr "" + +msgctxt "#30003" +msgid "Prefer styles from" +msgstr "" + +msgctxt "#30004" +msgid "Prefer moods from" +msgstr "" + +msgctxt "#30005" +msgid "Prefer themes from" +msgstr "" + +msgctxt "#30006" +msgid "Prefer rating from" +msgstr "" + +msgctxt "#30101" +msgid "Use Discogs.com" +msgstr "" + +msgctxt "#30102" +msgid "As fallback only" +msgstr "" + +msgctxt "#30103" +msgid "Always" +msgstr "" + +msgctxt "#30201" +msgid "If available, the album review will be downloaded in the selected language. It will fallback to english." +msgstr "" + +msgctxt "#30202" +msgid "Try to get genre info using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30203" +msgid "Try to get style info using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30204" +msgid "Try to get mood info using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30205" +msgid "Try to get theme info using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30206" +msgid "Try to get rating info using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30301" +msgid "Fallback: only use the discogs scraper if the album can't be found on MusicBrainz (faster, but could result in less complete results). Always: retrieve info from discogs.com for each album (slower, but results could be more complete)" +msgstr "" diff --git a/addons/metadata.generic.albums/resources/settings.xml b/addons/metadata.generic.albums/resources/settings.xml new file mode 100644 index 0000000000..51150c2c52 --- /dev/null +++ b/addons/metadata.generic.albums/resources/settings.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" ?> +<settings version="1"> + <section id="metadata.generic.albums"> + <category id="preferences" label="30000"> + <group id="1"> + <setting help="30201" id="lang" label="30001" type="string"> + <level>0</level> + <default>EN</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="CN">CN</option> + <option label="DE">DE</option> + <option label="EN">EN</option> + <option label="ES">ES</option> + <option label="FR">FR</option> + <option label="HU">HU</option> + <option label="IL">IL</option> + <option label="IT">IT</option> + <option label="JP">JP</option> + <option label="NL">NL</option> + <option label="NO">NO</option> + <option label="PL">PL</option> + <option label="PT">PT</option> + <option label="RU">RU</option> + <option label="SE">SE</option> + </options> + </constraints> + </setting> + <setting help="30202" id="genre" label="30002" type="string"> + <level>0</level> + <default>theaudiodb</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="allmusic">allmusic</option> + <option label="discogs">discogs</option> + <option label="theaudiodb">theaudiodb</option> + </options> + </constraints> + </setting> + <setting help="30203" id="style" label="30003" type="string"> + <level>0</level> + <default>theaudiodb</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="allmusic">allmusic</option> + <option label="discogs">discogs</option> + <option label="theaudiodb">theaudiodb</option> + </options> + </constraints> + </setting> + <setting help="30204" id="mood" label="30004" type="string"> + <level>0</level> + <default>theaudiodb</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="allmusic">allmusic</option> + <option label="theaudiodb">theaudiodb</option> + </options> + </constraints> + </setting> + <setting help="30205" id="theme" label="30005" type="string"> + <level>0</level> + <default>theaudiodb</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="allmusic">allmusic</option> + <option label="theaudiodb">theaudiodb</option> + </options> + </constraints> + </setting> + <setting help="30206" id="rating" label="30006" type="string"> + <level>0</level> + <default>musicbrainz</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="allmusic">allmusic</option> + <option label="discogs">discogs</option> + <option label="musicbrainz">musicbrainz</option> + <option label="theaudiodb">theaudiodb</option> + </options> + </constraints> + </setting> + </group> + </category> + <category id="options" label="33063"> + <group id="1"> + <setting help="30301" id="usediscogs" label="30101" type="integer"> + <level>0</level> + <default>0</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="30102">0</option> + <option label="30103">1</option> + </options> + </constraints> + </setting> + </group> + </category> + </section> +</settings> |