diff options
37 files changed, 2888 insertions, 272 deletions
diff --git a/addons/metadata.demo.albums/addon.xml b/addons/metadata.demo.albums/addon.xml deleted file mode 100644 index 023c22fc23..0000000000 --- a/addons/metadata.demo.albums/addon.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<addon id="metadata.demo.albums" - name="Demo albums python scraper" - version="1.0.0" - provider-name="spiff"> - <requires> - <import addon="xbmc.metadata" version="2.1.0"/> - </requires> - <extension point="xbmc.metadata.scraper.albums" - library="demo.py"/> - <extension point="xbmc.addon.metadata"> - <summary lang="en">Demo albums python scraper</summary> - <description lang="en">Demo albums python scraper.</description> - <platform>all</platform> - <license>GPL v2.0</license> - </extension> -</addon> diff --git a/addons/metadata.demo.albums/demo.py b/addons/metadata.demo.albums/demo.py deleted file mode 100644 index 8ff9542f39..0000000000 --- a/addons/metadata.demo.albums/demo.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- - -import xbmcplugin,xbmcgui,xbmc,xbmcaddon -import os,sys,urllib - -def get_params(): - param=[] - paramstring=sys.argv[2] - if len(paramstring)>=2: - params=sys.argv[2] - cleanedparams=params.replace('?','') - if (params[len(params)-1]=='/'): - params=params[0:len(params)-2] - pairsofparams=cleanedparams.split('&') - param={} - for i in range(len(pairsofparams)): - splitparams={} - splitparams=pairsofparams[i].split('=') - if (len(splitparams))==2: - param[splitparams[0]]=splitparams[1] - - return param - - -params=get_params() -print(params) - -try: - action=urllib.unquote_plus(params["action"]) -except: - pass - -print ("Action: "+action) - -if action == 'find': - try: - artist=urllib.unquote_plus(params["artist"]) - album=urllib.unquote_plus(params["title"]) - except: - pass - - print('Find album with title %s from artist %s' %(album, artist)) - liz=xbmcgui.ListItem('Demo album 1', thumbnailImage='DefaultAlbum.png', offscreen=True) - liz.setProperty('relevance', '0.5') - liz.setProperty('album.artist', artist) - liz.setProperty('album.year', '2005') - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url="/path/to/album", listitem=liz, isFolder=True) - - liz=xbmcgui.ListItem('Demo album 2', thumbnailImage='DefaultVideo.png', offscreen=True) - liz.setProperty('relevance', '0.3') - liz.setProperty('album.artist', 'spiff') - liz.setProperty('album.year', '2016') - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url="/path/to/album2", listitem=liz, isFolder=True) -elif action == 'getdetails': - try: - url=urllib.unquote_plus(params["url"]) - except: - pass - - if url == '/path/to/album': - liz=xbmcgui.ListItem('Demo album 1', offscreen=True) - liz.setProperty('album.musicbrainzid', '123') - liz.setProperty('album.artists', '2') - liz.setProperty('album.artist1.name', 'Jan') - liz.setProperty('album.artist1.musicbrainzid', '456') - liz.setProperty('album.artist2.name', 'Banan') - liz.setProperty('album.artist2.musicbrainzid', '789') - liz.setProperty('album.artist_description', 'I hate this album.') - liz.setProperty('album.genre', 'rock / pop') - liz.setProperty('album.styles', 'light / heavy') - liz.setProperty('album.moods', 'angry / happy') - liz.setProperty('album.themes', 'Morbid sexual things.. And urmumz.') - liz.setProperty('album.compiliation', 'true') - liz.setProperty('album.review', 'Somebody should die for making this') - liz.setProperty('album.release_date', '2005-01-02') - liz.setProperty('album.label', 'ArtistExploitation inc') - liz.setProperty('album.type', 'what is this?') - liz.setProperty('album.release_type', 'single') - liz.setProperty('album.year', '2005') - liz.setProperty('album.rating', '2.5') - liz.setProperty('album.userrating', '4.5') - liz.setProperty('album.votes', '100') - liz.setProperty('album.thumbs', '2') - liz.setProperty('album.thumb1.url', 'DefaultBackFanart.png') - liz.setProperty('album.thumb1.aspect', '1.78') - liz.setProperty('album.thumb2.url', '/home/akva/Pictures/hawaii-shirt.png') - liz.setProperty('album.thumb2.aspect', '2.35') - xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=liz) - elif url == '/path/to/album2': - liz=xbmcgui.ListItem('Demo album 2', offscreen=True) - liz.setProperty('album.musicbrainzid', '123') - liz.setProperty('album.artists', '2') - liz.setProperty('album.artist1.name', 'Heise') - liz.setProperty('album.artist1.musicbrainzid', '456') - liz.setProperty('album.artist2.name', 'Kran') - liz.setProperty('album.artist2.musicbrainzid', '789') - liz.setProperty('album.artist_description', 'I love this album.') - liz.setProperty('album.genre', 'classical / jazz') - liz.setProperty('album.styles', 'yay / hurrah') - liz.setProperty('album.moods', 'sad / excited') - liz.setProperty('album.themes', 'Nice things.. And unicorns.') - liz.setProperty('album.compiliation', 'false') - liz.setProperty('album.review', 'Somebody should be rewarded for making this') - liz.setProperty('album.release_date', '2015-01-02') - liz.setProperty('album.label', 'Artists inc') - liz.setProperty('album.type', 'what is that?') - liz.setProperty('album.release_type', 'album') - liz.setProperty('album.year', '2015') - liz.setProperty('album.rating', '4.5') - liz.setProperty('album.userrating', '3.5') - liz.setProperty('album.votes', '200') - liz.setProperty('album.thumbs', '2') - liz.setProperty('album.thumb1.url', 'DefaultBackFanart.png') - liz.setProperty('album.thumb1.aspect', '1.78') - liz.setProperty('album.thumb2.url', '/home/akva/Pictures/hawaii-shirt.png') - liz.setProperty('album.thumb2.aspect', '2.35') - xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=liz) - -xbmcplugin.endOfDirectory(int(sys.argv[1])) diff --git a/addons/metadata.demo.artists/addon.xml b/addons/metadata.demo.artists/addon.xml deleted file mode 100644 index 59b6f7d0bb..0000000000 --- a/addons/metadata.demo.artists/addon.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<addon id="metadata.demo.artists" - name="Demo artists python scraper" - version="1.0.0" - provider-name="spiff"> - <requires> - <import addon="xbmc.metadata" version="2.1.0"/> - </requires> - <extension point="xbmc.metadata.scraper.artists" - library="demo.py"/> - <extension point="xbmc.addon.metadata"> - <summary lang="en">Demo artists python scraper</summary> - <description lang="en">Demo artists python scraper</description> - <platform>all</platform> - <license>GPL v2.0</license> - </extension> -</addon> diff --git a/addons/metadata.demo.artists/demo.py b/addons/metadata.demo.artists/demo.py deleted file mode 100644 index 37a31bef65..0000000000 --- a/addons/metadata.demo.artists/demo.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- - -import xbmcplugin,xbmcgui,xbmc,xbmcaddon -import os,sys,urllib - -def get_params(): - param=[] - paramstring=sys.argv[2] - if len(paramstring)>=2: - params=sys.argv[2] - cleanedparams=params.replace('?','') - if (params[len(params)-1]=='/'): - params=params[0:len(params)-2] - pairsofparams=cleanedparams.split('&') - param={} - for i in range(len(pairsofparams)): - splitparams={} - splitparams=pairsofparams[i].split('=') - if (len(splitparams))==2: - param[splitparams[0]]=splitparams[1] - - return param - - -params=get_params() - -try: - action=urllib.unquote_plus(params["action"]) -except: - pass - -if action == 'find': - try: - artist=urllib.unquote_plus(params["artist"]) - except: - pass - - print('Find artist with name %s' %(artist)) - liz=xbmcgui.ListItem('Demo artist 1', thumbnailImage='DefaultAlbum.png', offscreen=True) - liz.setProperty('artist.genre', 'rock / pop') - liz.setProperty('artist.born', '2002') - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url="/path/to/artist", listitem=liz, isFolder=True) - - liz=xbmcgui.ListItem('Demo artist 2', thumbnailImage='DefaultAlbum.png', offscreen=True) - liz.setProperty('artist.genre', 'classical / jazz') - liz.setProperty('artist.born', '2012') - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url="/path/to/artist2", listitem=liz, isFolder=True) -elif action == 'resolveid': - liz=xbmcgui.ListItem(path='/path/to/artist2', offscreen=True) - xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=liz) -elif action == 'getdetails': - url=urllib.unquote_plus(params["url"]) - print('Artist with url %s' %(url)) - if url == '/path/to/artist': - liz=xbmcgui.ListItem('Demo artist 1', offscreen=True) - liz.setProperty('artist.musicbrainzid', '123') - liz.setProperty('artist.genre', 'rock / pop') - liz.setProperty('artist.styles', 'heavy / light') - liz.setProperty('artist.moods', 'angry / happy') - liz.setProperty('artist.years_active', '1980 / 2012') - liz.setProperty('artist.instruments', 'guitar / drums') - liz.setProperty('artist.born', '1/1/2001') - liz.setProperty('artist.formed', '1980') - liz.setProperty('artist.biography', 'Wrote lots of crap. Likes to torture cats.') - liz.setProperty('artist.died', 'Tomorrow.') - liz.setProperty('artist.disbanded', 'Dec 21 2012') - liz.setProperty('artist.fanarts', '2') - liz.setProperty('artist.fanart1.url', 'DefaultBackFanart.png') - liz.setProperty('artist.fanart1.preview', 'DefaultBackFanart.png') - liz.setProperty('artist.fanart1.dim', '720') - liz.setProperty('artist.fanart2.url', '/home/akva/Pictures/hawaii-shirt.png') - liz.setProperty('artist.fanart2.preview', '/home/akva/Pictures/hawaii-shirt.png') - liz.setProperty('artist.fanart2.dim', '1080') - liz.setProperty('artist.albums', '2') - liz.setProperty('artist.album1.title', 'Demo album 1') - liz.setProperty('artist.album1.year', '2002') - liz.setProperty('artist.album2.title', 'Demo album 2') - liz.setProperty('artist.album2.year', '2007') - liz.setProperty('artist.thumbs', '2') - liz.setProperty('artist.thumb1.url', 'DefaultBackFanart.png') - liz.setProperty('artist.thumb1.aspect', '1.78') - liz.setProperty('artist.thumb2.url', '/home/akva/Pictures/hawaii-shirt.png') - liz.setProperty('artist.thumb2.aspect', '2.35') - xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=liz) - if url == '/path/to/artist2': - liz=xbmcgui.ListItem('Demo artist 2', thumbnailImage='DefaultAlbum.png', offscreen=True) - liz.setProperty('artist.musicbrainzid', '456') - liz.setProperty('artist.genre', 'classical / jazz') - liz.setProperty('artist.styles', 'morbid / funny') - liz.setProperty('artist.moods', 'fast / dance') - liz.setProperty('artist.years_active', '1990 / 2016') - liz.setProperty('artist.instruments', 'bass / flute') - liz.setProperty('artist.born', '2/2/1971') - liz.setProperty('artist.formed', '1990') - liz.setProperty('artist.biography', 'Tortured lots of cats. Likes crap.') - liz.setProperty('artist.died', 'Yesterday.') - liz.setProperty('artist.disbanded', 'Nov 20 1980') - liz.setProperty('artist.fanarts', '2') - liz.setProperty('artist.fanart1.thumb', 'DefaultBackFanart.png') - liz.setProperty('artist.fanart1.dim', '720') - liz.setProperty('artist.fanart2.thumb', '/home/akva/Pictures/gnome-tshirt.png') - liz.setProperty('artist.fanart2.dim', '1080') - liz.setProperty('artist.albums', '2') - liz.setProperty('artist.album1.title', 'Demo album 1') - liz.setProperty('artist.album1.year', '2002') - liz.setProperty('artist.album2.title', 'Demo album 2') - liz.setProperty('artist.album2.year', '2005') - liz.setProperty('artist.thumbs', '2') - liz.setProperty('artist.thumb1.url', 'DefaultBackFanart.png') - liz.setProperty('artist.thumb1.aspect', '1.78') - liz.setProperty('artist.thumb2.url', '/home/akva/Pictures/hawaii-shirt.png') - liz.setProperty('artist.thumb2.aspect', '2.35') - xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=liz) - -xbmcplugin.endOfDirectory(int(sys.argv[1])) 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> diff --git a/addons/metadata.generic.artists/LICENSE.txt b/addons/metadata.generic.artists/LICENSE.txt new file mode 100644 index 0000000000..4f8e8eb30c --- /dev/null +++ b/addons/metadata.generic.artists/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.artists/addon.xml b/addons/metadata.generic.artists/addon.xml new file mode 100644 index 0000000000..723cda0dc2 --- /dev/null +++ b/addons/metadata.generic.artists/addon.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<addon id="metadata.generic.artists" name="Generic Artist 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.artists" library="default.py"/> + <extension point="xbmc.addon.metadata"> + <summary lang="en_GB">Generic music scraper for artists</summary> + <description lang="en_GB">Searches for artist information and artwork across multiple websites.</description> + <platform>all</platform> + <license>GPL-2.0-only</license> + <forum>https://forum.kodi.tv/showthread.php?tid=351571</forum> + <source>https://gitlab.com/ronie/metadata.generic.artists/</source> + <assets> + <icon>resources/icon.png</icon> + </assets> + <news>- first release</news> + </extension> +</addon> diff --git a/addons/metadata.generic.artists/changelog.txt b/addons/metadata.generic.artists/changelog.txt new file mode 100644 index 0000000000..a41e2803f2 --- /dev/null +++ b/addons/metadata.generic.artists/changelog.txt @@ -0,0 +1,22 @@ +v1.0.7 +- include alias and sortname in artist search +- filter inaccurate search results from discogs + +v1.0.6 +- provide musicbrainzreleasegroupid for artist albums + +v1.0.5 +- add artist gender +- disallow 0 value born/formed/died/disbanded dates from theaudiodb + +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.artists/default.py b/addons/metadata.generic.artists/default.py new file mode 100644 index 0000000000..c9a5a4b5d3 --- /dev/null +++ b/addons/metadata.generic.artists/default.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +import sys +from urllib.parse import parse_qsl +from lib.scraper import Scraper + + +class Main: + def __init__(self): + action, key, artist, url, nfo, settings = self._parse_argv() + Scraper(action, key, artist, 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', '') + # 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, url, nfo, settings + + +if (__name__ == '__main__'): + Main() diff --git a/addons/metadata.generic.artists/lib/allmusic.py b/addons/metadata.generic.artists/lib/allmusic.py new file mode 100644 index 0000000000..5bd1597c16 --- /dev/null +++ b/addons/metadata.generic.artists/lib/allmusic.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +import re + +def allmusic_artistdetails(data): + data = data.decode('utf-8') + artistdata = {} + artist = re.search(r'artist-name" itemprop="name">\s*(.*?)\s*<', data) + if artist: + artistdata['artist'] = artist.group(1) + else: + # no discography page available for this artist + return + active = re.search(r'class="active-dates">.*?<div>(.*?)<', data, re.S) + if active: + artistdata['active'] = active.group(1) + begin = re.search(r'class="birth">.*?<h4>\s*(.*?)\s*<', data, re.S) + if begin and begin.group(1) == 'Born': + born = re.search(r'class="birth">.*?<a.*?>(.*?)<', data, re.S) + if born: + artistdata['born'] = born.group(1) + elif begin and begin.group(1) == 'Formed': + formed = re.search(r'class="birth">.*?<a.*?>(.*?)<', data, re.S) + if formed: + artistdata['formed'] = formed.group(1) + end = re.search(r'class="died">.*?<h4>\s*(.*?)\s*<', data, re.S) + if end and end.group(1) == 'Died': + died = re.search(r'class="died">.*?<a.*?>(.*?)<', data, re.S) + if died: + artistdata['died'] = died.group(1) + elif end and end.group(1) == 'Disbanded': + disbanded = re.search(r'class="died">.*?<a.*?>(.*?)<', data, re.S) + if disbanded: + artistdata['disbanded'] = disbanded.group(1) + genre = re.search(r'class="genre">.*?<a.*?>(.*?)<', data, re.S) + if genre: + artistdata['genre'] = genre.group(1) + styledata = re.search(r'class="styles">.*?<div>\s*(.*?)\s*</div', data, re.S) + if styledata: + styles = re.findall(r'">(.*?)<', styledata.group(1)) + if styles: + artistdata['styles'] = ' / '.join(styles) + mooddata = re.search(r'class="moods">.*?<li>\s*(.*?)\s*</ul', data, re.S) + if mooddata: + moods = re.findall(r'">(.*?)<', mooddata.group(1)) + if moods: + artistdata['moods'] = ' / '.join(moods) + thumbsdata = re.search(r'class="artist-image">.*?<img src="(.*?)"', data, re.S) + if thumbsdata: + thumbs = [] + thumbdata = {} + thumb = thumbsdata.group(1).rstrip('?partner=allrovi.com') + # 0=largest / 1=75 / 2=150 / 3=250 / 4=400 / 5=500 / 6=1080 + if thumb.endswith('f=4'): + thumbdata['image'] = thumb.replace('f=4', 'f=0') + thumbdata['preview'] = thumb.replace('f=4', 'f=2') + else: + thumbdata['image'] = thumb + thumbdata['preview'] = thumb + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + artistdata['thumb'] = thumbs + return artistdata + +def allmusic_artistalbums(data): + data = data.decode('utf-8') + albums = [] + albumdata = re.search(r'tbody>\s*(.*?)\s*</tbody', data, re.S) + if albumdata: + albumlist = re.findall(r'tr.*?>\s*(.*?)\s*</tr', albumdata.group(1), re.S) + if albumlist: + for album in albumlist: + albumdata = {} + title = re.search(r'<a.*?>(.*?)<', album) + if title: + albumdata['title'] = title.group(1) + year = re.search(r'class="year".*?>\s*(.*?)\s*<', album) + if year: + albumdata['year'] = year.group(1) + else: + albumdata['year'] = '' + if albumdata: + albums.append(albumdata) + return albums diff --git a/addons/metadata.generic.artists/lib/discogs.py b/addons/metadata.generic.artists/lib/discogs.py new file mode 100644 index 0000000000..42fa1301d2 --- /dev/null +++ b/addons/metadata.generic.artists/lib/discogs.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import difflib + +def discogs_artistfind(data, artist): + artists = [] + for item in data.get('results',[]): + artistdata = {} + artistdata['artist'] = item['title'] + # filter inaccurate results + match = difflib.SequenceMatcher(None, artist.lower(), item['title'].lower()).ratio() + score = round(match, 2) + if score > 0.90: + artistdata['thumb'] = item['thumb'] + artistdata['genre'] = '' + artistdata['born'] = '' + artistdata['dcid'] = item['id'] + # discogs does not provide relevance, use our own + artistdata['relevance'] = str(score) + artists.append(artistdata) + return artists + +def discogs_artistdetails(data): + artistdata = {} + artistdata['artist'] = data['name'] + artistdata['biography'] = data['profile'] + if 'images' in data: + thumbs = [] + for item in data['images']: + thumbdata = {} + thumbdata['image'] = item['uri'] + thumbdata['preview'] = item['uri150'] + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + artistdata['thumb'] = thumbs + return artistdata + +def discogs_artistalbums(data): + albums = [] + for item in data['releases']: + if item['role'] == 'Main': + albumdata = {} + albumdata['title'] = item['title'] + albumdata['year'] = str(item.get('year', '')) + albums.append(albumdata) + return albums diff --git a/addons/metadata.generic.artists/lib/fanarttv.py b/addons/metadata.generic.artists/lib/fanarttv.py new file mode 100644 index 0000000000..22ae1f2e5c --- /dev/null +++ b/addons/metadata.generic.artists/lib/fanarttv.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +def fanarttv_artistart(data): + artistdata = {} + extras = [] + if 'artistbackground' in data: + fanart = [] + for item in data['artistbackground']: + fanartdata = {} + fanartdata['image'] = item['url'] + fanartdata['preview'] = item['url'].replace('/fanart/', '/preview/') + fanart.append(fanartdata) + artistdata['fanart'] = fanart + if 'artistthumb' in data: + thumbs = [] + for item in data['artistthumb']: + thumbdata = {} + thumbdata['image'] = item['url'] + thumbdata['preview'] = item['url'].replace('/fanart/', '/preview/') + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + if thumbs: + artistdata['thumb'] = thumbs + if 'musicbanner' in data: + artistdata['banner'] = data['musicbanner'][0]['url'] + for item in data['musicbanner']: + extradata = {} + extradata['image'] = item['url'] + extradata['preview'] = item['url'].replace('/fanart/', '/preview/') + extradata['aspect'] = 'banner' + extras.append(extradata) + if 'hdmusiclogo' in data: + artistdata['clearlogo'] = data['hdmusiclogo'][0]['url'] + for item in data['hdmusiclogo']: + extradata = {} + extradata['image'] = item['url'] + extradata['preview'] = item['url'].replace('/fanart/', '/preview/') + extradata['aspect'] = 'clearlogo' + extras.append(extradata) + elif 'musiclogo' in data: + artistdata['clearlogo'] = data['musiclogo'][0]['url'] + for item in data['musiclogo']: + extradata = {} + extradata['image'] = item['url'] + extradata['preview'] = item['url'].replace('/fanart/', '/preview/') + extradata['aspect'] = 'clearlogo' + extras.append(extradata) + if extras: + artistdata['extras'] = extras + return artistdata diff --git a/addons/metadata.generic.artists/lib/musicbrainz.py b/addons/metadata.generic.artists/lib/musicbrainz.py new file mode 100644 index 0000000000..4a9cbced0a --- /dev/null +++ b/addons/metadata.generic.artists/lib/musicbrainz.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +def musicbrainz_artistfind(data, artist): + artists = [] + for item in data.get('artists',[]): + artistdata = {} + artistdata['artist'] = item['name'] + artistdata['thumb'] = '' + artistdata['genre'] = '' + artistdata['born'] = item['life-span'].get('begin', '') + if 'type' in item: + artistdata['type'] = item['type'] + if 'gender' in item: + artistdata['gender'] = item['gender'] + if 'disambiguation' in item: + artistdata['disambiguation'] = item['disambiguation'] + artistdata['mbid'] = item['id'] + if item.get('score',1): + artistdata['relevance'] = str(item['score'] / 100.00) + artists.append(artistdata) + return artists + +def musicbrainz_artistdetails(data): + artistdata = {} + artistdata['artist'] = data['name'] + artistdata['mbartistid'] = data['id'] + artistdata['type'] = data['type'] + artistdata['gender'] = data['gender'] + artistdata['disambiguation'] = data['disambiguation'] + if data.get('life-span','') and data.get('type',''): + begin = data['life-span'].get('begin', '') + end = data['life-span'].get('end', '') + if data['type'] in ['Group', 'Orchestra', 'Choir']: + artistdata['formed'] = begin + artistdata['disbanded'] = end + elif data['type'] in ['Person', 'Character']: + artistdata['born'] = begin + artistdata['died'] = end + albums = [] + for item in data.get('release-groups',[]): + albumdata = {} + albumdata['title'] = item.get('title','') + albumdata['year'] = item.get('first-release-date','') + albumdata['musicbrainzreleasegroupid'] = item.get('id','') + albums.append(albumdata) + if albums: + artistdata['albums'] = albums + for item in data['relations']: + if item['type'] == 'allmusic': + artistdata['allmusic-url'] = item['url']['resource'] + elif item['type'] == 'discogs': + artistdata['discogs-url'] = item['url']['resource'] + return artistdata diff --git a/addons/metadata.generic.artists/lib/nfo.py b/addons/metadata.generic.artists/lib/nfo.py new file mode 100644 index 0000000000..7aeb7ff06c --- /dev/null +++ b/addons/metadata.generic.artists/lib/nfo.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +import re + +def nfo_geturl(data): + result = re.search('https://musicbrainz.org/(ws/2/)?artist/([0-9a-z\-]*)', data) + if result: + return result.group(2) diff --git a/addons/metadata.generic.artists/lib/scraper.py b/addons/metadata.generic.artists/lib/scraper.py new file mode 100644 index 0000000000..8828a435c5 --- /dev/null +++ b/addons/metadata.generic.artists/lib/scraper.py @@ -0,0 +1,415 @@ +# -*- 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_artistdetails +from .theaudiodb import theaudiodb_artistalbums +from .musicbrainz import musicbrainz_artistfind +from .musicbrainz import musicbrainz_artistdetails +from .discogs import discogs_artistfind +from .discogs import discogs_artistdetails +from .discogs import discogs_artistalbums +from .allmusic import allmusic_artistdetails +from .allmusic import allmusic_artistalbums +from .nfo import nfo_geturl +from .fanarttv import fanarttv_artistart +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, 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 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_artist(artist, 'musicbrainz') + if result: + self.return_search(result) + # fallback to discogs + else: + result = self.find_artist(artist, 'discogs') + if result: + self.return_search(result) + # return info using artistname / id's + elif action == 'getdetails': + details = {} + url = json.loads(url) + artist = url['artist'].encode('utf-8') + mbid = url.get('mbid', '') + dcid = url.get('dcid', '') + threads = [] + extrathreads = [] + # we have a musicbrainz id + if mbid: + # musicbrainz allows 1 api per second. + RATELIMIT = 1000 + scrapers = [[mbid, 'musicbrainz'], [mbid, 'theaudiodb'], [mbid, 'fanarttv']] + + for item in scrapers: + 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: + extrascrapers = [] + # only scrape allmusic if we have an url provided by musicbrainz + if 'allmusic-url' in details['musicbrainz']: + extrascrapers.append([details['musicbrainz']['allmusic-url'], 'allmusic']) + # only scrape discogs if we have an url provided by musicbrainz and discogs scraping is explicitly enabled (as it is slower) + if 'discogs-url' in details['musicbrainz'] and self.usediscogs == 1: + dcid = int(details['musicbrainz']['discogs-url'].rsplit('/', 1)[1]) + extrascrapers.append([dcid, 'discogs']) + # discogs allows 1 api per second. this query requires 2 discogs api calls + RATELIMIT = 2000 + for item in extrascrapers: + thread = Thread(target = self.get_details, args = (item[0], item[1], details)) + extrathreads.append(thread) + thread.start() + # we have a discogs id + else: + result = self.get_details(dcid, 'discogs', details) + # discogs allow 1 api per second. this query requires 2 discogs api call + RATELIMIT = 2000 + if threads: + for thread in threads: + thread.join() + if extrathreads: + for thread in extrathreads: + thread.join() + result = self.compile_results(details) + if result: + self.return_details(result) + elif action == 'NfoUrl': + mbid = nfo_geturl(nfo) + if mbid: + 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.bio = settings['bio'] + self.discog = settings['discog'] + self.genre = settings['genre'] + self.lang = settings['lang'] + self.mood = settings['mood'] + self.style = settings['style'] + self.usediscogs = settings['usediscogs'] + + def resolve_mbid(self, mbid): + # create dummy result + item = {} + item['artist'] = '' + item['mbartistid'] = mbid + return item + + def find_artist(self, artist, site): + json = True + # musicbrainz + if site == 'musicbrainz': + url = MUSICBRAINZURL % (MUSICBRAINZSEARCH % urllib.parse.quote_plus(artist)) + scraper = musicbrainz_artistfind + # musicbrainz + if site == 'discogs': + url = DISCOGSURL % (DISCOGSSEARCH % (urllib.parse.quote_plus(artist), DISCOGSKEY , DISCOGSSECRET)) + scraper = discogs_artistfind + result = get_data(url, json) + if not result: + return + artistresults = scraper(result, artist) + return artistresults + + def get_details(self, param, site, details): + json = True + # theaudiodb + if site == 'theaudiodb': + url = AUDIODBURL % (AUDIODBKEY, AUDIODBDETAILS % param) + artistscraper = theaudiodb_artistdetails + # musicbrainz + elif site == 'musicbrainz': + url = MUSICBRAINZURL % (MUSICBRAINZDETAILS % param) + artistscraper = musicbrainz_artistdetails + # fanarttv + elif site == 'fanarttv': + url = FANARTVURL % (param, FANARTVKEY) + artistscraper = fanarttv_artistart + # discogs + elif site == 'discogs': + url = DISCOGSURL % (DISCOGSDETAILS % (param, DISCOGSKEY, DISCOGSSECRET)) + artistscraper = discogs_artistdetails + # allmusic + elif site == 'allmusic': + url = param + '/discography' + artistscraper = allmusic_artistdetails + json = False + result = get_data(url, json) + if not result: + return + artistresults = artistscraper(result) + if not artistresults: + return + if site == 'theaudiodb' or site == 'discogs' or site == 'allmusic': + if site == 'theaudiodb': + # theaudiodb - discography + albumsurl = AUDIODBURL % (AUDIODBKEY, AUDIODBDISCOGRAPHY % artistresults['mbartistid']) + scraper = theaudiodb_artistalbums + elif site == 'discogs': + # discogs - discography + albumsurl = DISCOGSURL % (DISCOGSDISCOGRAPHY % (param, DISCOGSKEY, DISCOGSSECRET)) + scraper = discogs_artistalbums + elif site == 'allmusic': + # allmusic - discography + albumsurl = param + '/discography' + scraper = allmusic_artistalbums + albumdata = get_data(albumsurl, json) + if albumdata: + albumresults = scraper(albumdata) + if albumresults: + artistresults['albums'] = albumresults + details[site] = artistresults + return details + + def compile_results(self, details): + result = {} + thumbs = [] + fanart = [] + 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) + elif k == 'fanart': + fanart.append(v) + if k == 'extras': + extras.append(v) + if 'musicbrainz' in details: + for k, v in details['musicbrainz'].items(): + result[k] = v + if 'fanarttv' in details: + for k, v in details['fanarttv'].items(): + result[k] = v + if k == 'thumb': + thumbs.append(v) + elif k == 'fanart': + fanart.append(v) + if k == 'extras': + extras.append(v) + # provide artwork from all scrapers for getthumb / getfanart option + if result: + # artworks from most accurate sources first + thumbs.reverse() + thumbnails = [] + fanart.reverse() + fanarts = [] + # the order for extra art does not matter + extraart = [] + for thumblist in thumbs: + for item in thumblist: + thumbnails.append(item) + 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 + for fanartlist in fanart: + for item in fanartlist: + fanarts.append(item) + result['fanart'] = fanarts + data = self.user_prefs(details, result) + return data + + def user_prefs(self, details, result): + # user preferences + lang = 'biography' + self.lang + if self.bio == 'theaudiodb' and 'theaudiodb' in details: + if lang in details['theaudiodb']: + result['biography'] = details['theaudiodb'][lang] + elif 'biographyEN' in details['theaudiodb']: + result['biography'] = details['theaudiodb']['biographyEN'] + elif self.bio == 'discogs' and 'discogs' in details: + result['biography'] = details['discogs']['biography'] + if (self.discog in details) and ('albums' in details[self.discog]): + result['albums'] = details[self.discog]['albums'] + 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'] + return result + + def return_search(self, data): + for item in data: + listitem = xbmcgui.ListItem(item['artist'], offscreen=True) + listitem.setArt({'thumb': item['thumb']}) + listitem.setProperty('artist.genre', item['genre']) + listitem.setProperty('artist.born', item['born']) + listitem.setProperty('relevance', item['relevance']) + if 'type' in item: + listitem.setProperty('artist.type', item['type']) + if 'gender' in item: + listitem.setProperty('artist.gender', item['gender']) + if 'disambiguation' in item: + listitem.setProperty('artist.disambiguation', item['disambiguation']) + url = {'artist':item['artist']} + if 'mbid' in item: + url['mbid'] = item['mbid'] + if 'dcid' in item: + url['dcid'] = item['dcid'] + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=json.dumps(url), listitem=listitem, isFolder=True) + + def return_nfourl(self, item): + url = {'artist':item['artist'], 'mbid':item['mbartistid']} + 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'], 'mbid':item['mbartistid']} + 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 'artist' in item: + return + listitem = xbmcgui.ListItem(item['artist'], offscreen=True) + if 'mbartistid' in item: + listitem.setProperty('artist.musicbrainzid', item['mbartistid']) + if 'genre' in item: + listitem.setProperty('artist.genre', item['genre']) + if 'biography' in item: + listitem.setProperty('artist.biography', item['biography']) + if 'gender' in item: + listitem.setProperty('artist.gender', item['gender']) + if 'styles' in item: + listitem.setProperty('artist.styles', item['styles']) + if 'moods' in item: + listitem.setProperty('artist.moods', item['moods']) + if 'instruments' in item: + listitem.setProperty('artist.instruments', item['instruments']) + if 'disambiguation' in item: + listitem.setProperty('artist.disambiguation', item['disambiguation']) + if 'type' in item: + listitem.setProperty('artist.type', item['type']) + if 'sortname' in item: + listitem.setProperty('artist.sortname', item['sortname']) + if 'active' in item: + listitem.setProperty('artist.years_active', item['active']) + if 'born' in item: + listitem.setProperty('artist.born', item['born']) + if 'formed' in item: + listitem.setProperty('artist.formed', item['formed']) + if 'died' in item: + listitem.setProperty('artist.died', item['died']) + if 'disbanded' in item: + listitem.setProperty('artist.disbanded', item['disbanded']) + art = {} + if 'clearlogo' in item: + art['clearlogo'] = item['clearlogo'] + if 'banner' in item: + art['banner'] = item['banner'] + if 'clearart' in item: + art['clearart'] = item['clearart'] + if 'landscape' in item: + art['landscape'] = item['landscape'] + listitem.setArt(art) + if 'fanart' in item: + listitem.setProperty('artist.fanarts', str(len(item['fanart']))) + for count, fanart in enumerate(item['fanart']): + listitem.setProperty('artist.fanart%i.url' % (count + 1), fanart['image']) + listitem.setProperty('artist.fanart%i.preview' % (count + 1), fanart['preview']) + if 'thumb' in item: + listitem.setProperty('artist.thumbs', str(len(item['thumb']))) + for count, thumb in enumerate(item['thumb']): + listitem.setProperty('artist.thumb%i.url' % (count + 1), thumb['image']) + listitem.setProperty('artist.thumb%i.preview' % (count + 1), thumb['preview']) + listitem.setProperty('artist.thumb%i.aspect' % (count + 1), thumb['aspect']) + if 'albums' in item: + listitem.setProperty('artist.albums', str(len(item['albums']))) + for count, album in enumerate(item['albums']): + listitem.setProperty('artist.album%i.title' % (count + 1), album['title']) + listitem.setProperty('artist.album%i.year' % (count + 1), album['year']) + if 'musicbrainzreleasegroupid' in album: + listitem.setProperty('artist.album%i.musicbrainzreleasegroupid' % (count + 1), album['musicbrainzreleasegroupid']) + xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=listitem) diff --git a/addons/metadata.generic.artists/lib/theaudiodb.py b/addons/metadata.generic.artists/lib/theaudiodb.py new file mode 100644 index 0000000000..c2a80a5614 --- /dev/null +++ b/addons/metadata.generic.artists/lib/theaudiodb.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +def theaudiodb_artistdetails(data): + if data.get('artists',[]): + item = data['artists'][0] + artistdata = {} + extras = [] + artistdata['artist'] = item['strArtist'] + # api inconsistent + if item.get('intFormedYear','') and item['intFormedYear'] != '0': + artistdata['formed'] = item['intFormedYear'] + if item.get('intBornYear','') and item['intBornYear'] != '0': + artistdata['born'] = item['intBornYear'] + if item.get('intDiedYear','') and item['intDiedYear'] != '0': + artistdata['died'] = item['intDiedYear'] + if item.get('strDisbanded','') and item['strDisbanded'] != '0': + artistdata['disbanded'] = item['strDisbanded'] + if item.get('strStyle',''): + artistdata['styles'] = item['strStyle'] + if item.get('strGenre',''): + artistdata['genre'] = item['strGenre'] + if item.get('strMood',''): + artistdata['moods'] = item['strMood'] + if item.get('strGender',''): + artistdata['gender'] = item['strGender'] + if item.get('strBiographyEN',''): + artistdata['biographyEN'] = item['strBiographyEN'] + if item.get('strBiographyDE',''): + artistdata['biographyDE'] = item['strBiographyDE'] + if item.get('strBiographyFR',''): + artistdata['biographyFR'] = item['strBiographyFR'] + if item.get('strBiographyCN',''): + artistdata['biographyCN'] = item['strBiographyCN'] + if item.get('strBiographyIT',''): + artistdata['biographyIT'] = item['strBiographyIT'] + if item.get('strBiographyJP',''): + artistdata['biographyJP'] = item['strBiographyJP'] + if item.get('strBiographyRU',''): + artistdata['biographyRU'] = item['strBiographyRU'] + if item.get('strBiographyES',''): + artistdata['biographyES'] = item['strBiographyES'] + if item.get('strBiographyPT',''): + artistdata['biographyPT'] = item['strBiographyPT'] + if item.get('strBiographySE',''): + artistdata['biographySE'] = item['strBiographySE'] + if item.get('strBiographyNL',''): + artistdata['biographyNL'] = item['strBiographyNL'] + if item.get('strBiographyHU',''): + artistdata['biographyHU'] = item['strBiographyHU'] + if item.get('strBiographyNO',''): + artistdata['biographyNO'] = item['strBiographyNO'] + if item.get('strBiographyIL',''): + artistdata['biographyIL'] = item['strBiographyIL'] + if item.get('strBiographyPL',''): + artistdata['biographyPL'] = item['strBiographyPL'] + if item.get('strMusicBrainzID',''): + artistdata['mbartistid'] = item['strMusicBrainzID'] + if item.get('strArtistFanart',''): + fanart = [] + fanartdata = {} + fanartdata['image'] = item['strArtistFanart'] + fanartdata['preview'] = item['strArtistFanart'] + '/preview' + fanart.append(fanartdata) + if item['strArtistFanart2']: + fanartdata = {} + fanartdata['image'] = item['strArtistFanart2'] + fanartdata['preview'] = item['strArtistFanart2'] + '/preview' + fanart.append(fanartdata) + if item['strArtistFanart3']: + fanartdata = {} + fanartdata['image'] = item['strArtistFanart3'] + fanartdata['preview'] = item['strArtistFanart3'] + '/preview' + fanart.append(fanartdata) + artistdata['fanart'] = fanart + if item.get('strArtistThumb',''): + thumbs = [] + thumbdata = {} + thumbdata['image'] = item['strArtistThumb'] + thumbdata['preview'] = item['strArtistThumb'] + '/preview' + thumbdata['aspect'] = 'thumb' + thumbs.append(thumbdata) + artistdata['thumb'] = thumbs + if item.get('strArtistLogo',''): + artistdata['clearlogo'] = item['strArtistLogo'] + extradata = {} + extradata['image'] = item['strArtistLogo'] + extradata['preview'] = item['strArtistLogo'] + '/preview' + extradata['aspect'] = 'clearlogo' + extras.append(extradata) + if item.get('strArtistClearart',''): + artistdata['clearart'] = item['strArtistClearart'] + extradata = {} + extradata['image'] = item['strArtistClearart'] + extradata['preview'] = item['strArtistClearart'] + '/preview' + extradata['aspect'] = 'clearart' + extras.append(extradata) + if item.get('strArtistWideThumb',''): + artistdata['landscape'] = item['strArtistWideThumb'] + extradata = {} + extradata['image'] = item['strArtistWideThumb'] + extradata['preview'] = item['strArtistWideThumb'] + '/preview' + extradata['aspect'] = 'landscape' + extras.append(extradata) + if item.get('strArtistBanner',''): + artistdata['banner'] = item['strArtistBanner'] + extradata = {} + extradata['image'] = item['strArtistBanner'] + extradata['preview'] = item['strArtistBanner'] + '/preview' + extradata['aspect'] = 'banner' + extras.append(extradata) + if extras: + artistdata['extras'] = extras + return artistdata + +def theaudiodb_artistalbums(data): + albums = [] + albumlist = data.get('album',[]) + if albumlist: + for item in data.get('album',[]): + albumdata = {} + albumdata['title'] = item['strAlbum'] + albumdata['year'] = item.get('intYearReleased', '') + albums.append(albumdata) + return albums diff --git a/addons/metadata.generic.artists/lib/utils.py b/addons/metadata.generic.artists/lib/utils.py new file mode 100644 index 0000000000..5d432f01f5 --- /dev/null +++ b/addons/metadata.generic.artists/lib/utils.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +AUDIODBKEY = '58424d43204d6564696120' +AUDIODBURL = 'https://www.theaudiodb.com/api/v1/json/%s/%s' +AUDIODBSEARCH = 'search.php?s=%s' +AUDIODBDETAILS = 'artist-mb.php?i=%s' +AUDIODBDISCOGRAPHY = 'discography-mb.php?s=%s' + +MUSICBRAINZURL = 'https://musicbrainz.org/ws/2/artist/%s' +MUSICBRAINZSEARCH = '?query="%s"&fmt=json' +MUSICBRAINZDETAILS = '%s?inc=url-rels+release-groups&type=album&fmt=json' + +DISCOGSKEY = 'zACPgktOmNegwbwKWMaC' +DISCOGSSECRET = 'wGuSOeMtfdkQxtERKQKPquyBwExSHdQq' +DISCOGSURL = 'https://api.discogs.com/%s' +DISCOGSSEARCH = 'database/search?q=%s&type=artist&key=%s&secret=%s' +DISCOGSDETAILS = 'artists/%i?key=%s&secret=%s' +DISCOGSDISCOGRAPHY = 'artists/%i/releases?sort=format&page=1&per_page=100&key=%s&secret=%s' + +ALLMUSICURL = 'https://www.allmusic.com/%s' +ALLMUSICSEARCH = 'search/artists/%s' + +FANARTVKEY = 'ed4b784f97227358b31ca4dd966a04f1' +FANARTVURL = 'https://webservice.fanart.tv/v3/music/%s?api_key=%s' diff --git a/addons/metadata.generic.artists/resources/icon.png b/addons/metadata.generic.artists/resources/icon.png Binary files differnew file mode 100644 index 0000000000..25fa698f67 --- /dev/null +++ b/addons/metadata.generic.artists/resources/icon.png diff --git a/addons/metadata.generic.artists/resources/language/resource.language.en_gb/strings.po b/addons/metadata.generic.artists/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..5c4998f6ff --- /dev/null +++ b/addons/metadata.generic.artists/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,85 @@ +# Kodi Media Center language file +# Addon Name: Generic Artist Scraper +# Addon id: metadata.generic.artists +# 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 artist biography" +msgstr "" + +msgctxt "#30002" +msgid "Prefer biography from" +msgstr "" + +msgctxt "#30003" +msgid "Prefer discography from" +msgstr "" + +msgctxt "#30004" +msgid "Prefer genres from" +msgstr "" + +msgctxt "#30005" +msgid "Prefer styles from" +msgstr "" + +msgctxt "#30006" +msgid "Prefer moods 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 artist biography will be downloaded in the selected language. It will fallback to english." +msgstr "" + +msgctxt "#30202" +msgid "Try to get the artist biography using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30203" +msgid "Try to get the artist discography using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30204" +msgid "Try to get genre info using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30205" +msgid "Try to get style info using the selected scraper. Other scrapers will be used if the prefered scraper returns no results." +msgstr "" + +msgctxt "#30206" +msgid "Try to get mood 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 artist can't be found on MusicBrainz (faster, but could result in less complete results). Always: retrieve info from discogs.com for each artist (slower, but results could be more complete)" +msgstr "" diff --git a/addons/metadata.generic.artists/resources/settings.xml b/addons/metadata.generic.artists/resources/settings.xml new file mode 100644 index 0000000000..53508b1be9 --- /dev/null +++ b/addons/metadata.generic.artists/resources/settings.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" ?> +<settings version="1"> + <section id="metadata.generic.artists"> + <category id="general" 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="bio" label="30002" type="string"> + <level>0</level> + <default>theaudiodb</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="discogs">discogs</option> + <option label="theaudiodb">theaudiodb</option> + </options> + </constraints> + </setting> + <setting help="30203" id="discog" label="30003" type="string"> + <level>0</level> + <default>allmusic</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> + <setting help="30204" id="genre" label="30004" type="string"> + <level>0</level> + <default>allmusic</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="style" label="30005" type="string"> + <level>0</level> + <default>allmusic</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="mood" label="30006" type="string"> + <level>0</level> + <default>allmusic</default> + <control format="string" type="spinner"/> + <constraints> + <options> + <option label="allmusic">allmusic</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> diff --git a/cmake/installdata/common/addons.txt b/cmake/installdata/common/addons.txt index 13dbab39ec..897420faa7 100644 --- a/cmake/installdata/common/addons.txt +++ b/cmake/installdata/common/addons.txt @@ -39,6 +39,8 @@ addons/metadata.common.imdb.com/* addons/metadata.common.musicbrainz.org/* addons/metadata.common.theaudiodb.com/* addons/metadata.common.themoviedb.org/* +addons/metadata.generic.albums/* +addons/metadata.generic.artists/* addons/metadata.themoviedb.org/* addons/metadata.tvshows.themoviedb.org/* addons/kodi.vfs/* diff --git a/system/addon-manifest.xml b/system/addon-manifest.xml index abeb5ec60c..40732c8d18 100644 --- a/system/addon-manifest.xml +++ b/system/addon-manifest.xml @@ -30,6 +30,8 @@ <addon>metadata.common.musicbrainz.org</addon> <addon>metadata.common.theaudiodb.com</addon> <addon>metadata.common.themoviedb.org</addon> + <addon>metadata.generic.albums</addon> + <addon>metadata.generic.artists</addon> <addon>metadata.local</addon> <addon>metadata.themoviedb.org</addon> <addon>metadata.tvshows.themoviedb.org</addon> diff --git a/system/settings/settings.xml b/system/settings/settings.xml index 671fe325c9..def3740936 100755 --- a/system/settings/settings.xml +++ b/system/settings/settings.xml @@ -1101,7 +1101,7 @@ </setting> <setting id="musiclibrary.albumsscraper" type="addon" label="20193" help="36257"> <level>1</level> - <default>metadata.album.universal</default> + <default>metadata.generic.albums</default> <constraints> <addontype>xbmc.metadata.scraper.albums</addontype> </constraints> @@ -1111,7 +1111,7 @@ </setting> <setting id="musiclibrary.artistsscraper" type="addon" label="20194" help="36258"> <level>1</level> - <default>metadata.artists.universal</default> + <default>metadata.generic.artists</default> <constraints> <addontype>xbmc.metadata.scraper.artists</addontype> </constraints> |