From f17a18b92f5b07e74744fbdaec08e32d05d5271f Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Wed, 24 Jan 2024 23:23:14 -0600 Subject: [PATCH] metadata/Music: import latest CU LRC lyrics grabbers except for files not needed: resources/, addon.xml, default,py and README.txt. MythMusic uses only the LyricsFetchers in lib/culrcscrapers/*/lyricsScraper.py --- .../scripts/metadata/Music/lyrics/.gitignore | 8 + .../scripts/metadata/Music/lyrics/LICENSE.txt | 282 ++++++++++ .../metadata/Music/lyrics/changelog.txt | 488 ++++++++++++++++++ .../metadata/Music/lyrics/lib/audiofile.py | 127 +++++ .../lib/broken-scrapers/alsong/__init__.py | 1 + .../broken-scrapers/alsong/lyricsScraper.py | 65 +++ .../lib/broken-scrapers/baidu/__init__.py | 1 + .../broken-scrapers/baidu/lyricsScraper.py | 51 ++ .../lib/broken-scrapers/gomaudio/__init__.py | 1 + .../broken-scrapers/gomaudio/lyricsScraper.py | 101 ++++ .../lib/broken-scrapers/lyricwiki/__init__.py | 1 + .../lyricwiki/lyricsScraper.py | 67 +++ .../broken-scrapers/minilyrics/__init__.py | 1 + .../minilyrics/lyricsScraper.py | 161 ++++++ .../lib/broken-scrapers/ttplayer/__init__.py | 1 + .../broken-scrapers/ttplayer/lyricsScraper.py | 207 ++++++++ .../lib/broken-scrapers/xiami/__init__.py | 1 + .../broken-scrapers/xiami/lyricsScraper.py | 96 ++++ .../lyrics/lib/culrcscrapers/__init__.py | 1 + .../lib/culrcscrapers/azlyrics/__init__.py | 1 + .../culrcscrapers/azlyrics/lyricsScraper.py | 42 ++ .../lib/culrcscrapers/darklyrics/__init__.py | 1 + .../culrcscrapers/darklyrics/lyricsScraper.py | 124 +++++ .../lib/culrcscrapers/genius/__init__.py | 1 + .../lib/culrcscrapers/genius/lyricsScraper.py | 68 +++ .../lib/culrcscrapers/lrclib/__init__.py | 1 + .../lib/culrcscrapers/lrclib/lyricsScraper.py | 66 +++ .../lib/culrcscrapers/lyricscom/__init__.py | 1 + .../culrcscrapers/lyricscom/lyricsScraper.py | 61 +++ .../culrcscrapers/lyricsify/lyricsScraper.py | 75 +++ .../lib/culrcscrapers/lyricsmode/__init__.py | 1 + .../culrcscrapers/lyricsmode/lyricsScraper.py | 59 +++ .../lib/culrcscrapers/megalobiz/__init__.py | 1 + .../culrcscrapers/megalobiz/lyricsScraper.py | 69 +++ .../lib/culrcscrapers/music163/__init__.py | 1 + .../culrcscrapers/music163/lyricsScraper.py | 71 +++ .../lib/culrcscrapers/musixmatch/__init__.py | 1 + .../culrcscrapers/musixmatch/lyricsScraper.py | 86 +++ .../culrcscrapers/musixmatchlrc/__init__.py | 1 + .../musixmatchlrc/lyricsScraper.py | 117 +++++ .../lib/culrcscrapers/supermusic/__init__.py | 1 + .../culrcscrapers/supermusic/lyricsScraper.py | 64 +++ .../metadata/Music/lyrics/lib/embedlrc.py | 183 +++++++ .../metadata/Music/lyrics/lib/scrapertest.py | 268 ++++++++++ .../metadata/Music/lyrics/lib/utils.py | 186 +++++++ 45 files changed, 3211 insertions(+) create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/.gitignore create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/LICENSE.txt create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/audiofile.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/__init__.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/embedlrc.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/.gitignore b/mythtv/programs/scripts/metadata/Music/lyrics/.gitignore new file mode 100644 index 00000000000..0164cc0d587 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/.gitignore @@ -0,0 +1,8 @@ +# CU LRC files not needed in MythMusic: +addon.xml +default.py +README.txt +resources + +# twitham's extra files not needed: +filetweak.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/LICENSE.txt b/mythtv/programs/scripts/metadata/Music/lyrics/LICENSE.txt new file mode 100644 index 00000000000..4f8e8eb30cc --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/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/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt b/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt new file mode 100644 index 00000000000..50c2e60ade1 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt @@ -0,0 +1,488 @@ +v6.6.1 +- add supermusic scraper + +v6.6.0 +- removed minilyrics +- removed gomaudio +- fixed lyricsify +- fixed lyricscom +- fixed azlyrics +- added lrclib +- added megalobiz +- added musixmatch lrc + +v6.5.2 +- remove invalid characters from filenames + +v6.5.1 +- fix crash if ctypes are not supported + +v6.5.0 +- add musixmatch scraper + +v6.4.1 +- fix genius scraper +- fix darklyrics scraper + +v6.4.0 +- replace syair by lyricsify + +v6.3.14 +- fix Genius scraper + +v6.3.13 +- add separate options for writing lrc and txt lyrics + +v6.3.12 +- add separate options for reading .lrc and .txt files + +v6.3.11 +- fix syair scraper + +v6.3.10 +- remove quotes from filename + +v6.3.9 +- fix embedded lyrics + +v6.3.8 +- update japanese language file + +v6.3.7 +- python 3.9 compatibility + +v6.3.6 +- display artist name and track title in the lyrics dialog +- add option to hide the lyrics background +- before searching for lyrics, check if the song hasn't changed + +v6.3.5 +- don't search for next lyrics if we've already skipped to another track + +v6.3.4 +- fix potential crash in minlyrics scraper + +v6.3.3 +- fix clear lyric list when next song starts + +v6.3.2 +- fix lyric selection + +v6.3.1 +- skin cosmetics +- move all scrapers to python requests +- fix bug with monitor class + +v6.3.0 +- remove the need for list control 120 +- remove support for manually searching for lyrics +- don't use window properties for inter-thread communication +- remove thread locking + +v6.2.4 +- fix darklyrics scraper +- really really fix reading embedded lyrics from remote files + +v6.2.3 +- add support for MusicPlayer.Lyrics infolabel +- support lyrics from .ogg files +- support lyrics from .ape files +- really fix reading embedded lyrics from remote files +- fix getting lyrics from memory + +v6.2.2 +- fix reading embedded lyrics from remote files + +v6.2.1 +- remove broken ttplayer scraper +- fix genius scraper +- fix manual sync +- fix potential crash in minilyrics + +v6.2.0 +- refactor +- update mutagen + +v6.1.0 +- really add syair scraper + +v6.0.10 +- Player.IsInternetStream bug workaround + +v6.0.9 +- don't fetch lyrics twice on windowopen and simultanious avstarted call + +v6.0.8 +- add syair scraper + +v6.0.7 +- fix osd would interfere with getting page lines + +v6.0.6 +- break from loop + +v6.0.5 +- fix music163 error + +v6.0.4 +- handle dialog.close() +- create listitems offscreen + +v6.0.3 +- use threading lock + +v6.0.2 +- don't display/focus the first line from the start of the song + +v6.0.1 +- bump + +v5.5.14 +- stop searching for lyrics when exiting the visualization screen + +v5.5.13 +- fix negative offset + +v6.0.0 +- changes for python 3 + +v5.5.10 +- delete lyrics from memory as well +- re-search for lyrics if it was not found previously +- fix lyrics from memory +- fix lyricwiki scraper +- more accurate results from ttplayer scraper +- genius scraper strip blank lines +- change manual sync range to +/ 20 secs + +v5.5.9 +- fix lyricsmode scraper + +v5.5.8 +- cosmetics + +v5.5.7 +- remove xiami scraper + +v5.5.6 +- language update + +v5.5.5 +- fix ttplayer scraper +- fix lyricsmode scraper + +v5.5.4 +- fix xiami error + +v5.5.3 +- handle space in offset tag + +v5.5.2 +- move repo to gitlab + +v5.5.1 +- add azlyrics scraper +- re-add minilyrics scraper +- fix xiami scraper + +v5.5.0 +- remove broken scrapers + +v5.4.8 +- fix letssingit scraper +- fix pvr radio + +v5.4.7 +- fix letssingit scraper +- fix xiami scraper + +v5.4.6 +- fix embedded uslt lyrics + +v5.4.5 +- add test for xiami scraper + +v5.4.4 +- add xiami scraper + +v5.4.3 +- fix letssingit scraper + +v5.4.2 +- fix letssingit scraper + +v5.4.1 +- fix letssingit scraper + +v5.4.0 +- gomaudio: fix handling of accented characters +- fix letssingit scraper +- fix genius scraper + +v5.3.9 +- fix letssingit scraper +- fix getting song title from internet streams +- don't crash on offset tags without value + +v5.3.8 +- cosmetics + +v5.3.7 +- add support for synced lyrics in txxx tag + +v5.3.6 +- search local file even without song title + +v5.3.5 +- filter more lines + +v5.3.4 +- filter 'attribution' lines from lyrics + +v5.3.3 +- added option to delete lyrics file + +v5.3.2 +- save manual sync offset to lrc file + +v5.3.1 +- silence notifications +- highlight selected lyric in list + +v5.3.0 +- add manual sync option + +v5.2.6 +- fix incorrect results from gomaudio for streaming audio + +v5.2.5 +- add global offset option + +v5.2.4 +- offset needs to be substracted from the timestamp + +v5.2.3 +- cosmetics + +v5.2.2 +- add support for lrc offset + +v5.2.1 +- sync lrc lyrics with streaming radio + +v5.2.0 +- add lyrics.com scraper +- add letssingit scraper + +v5.1.2 +- fix minilyrics scraper + +v5.1.1 +- fix baidu lrc scraper +- fix alsong lrc scraper + +v5.1.0 +- fix broken text scrapers + +v5.0.9 +- improve stripping of korean text +- fix parsing lrc timestamps +- strip lines with duplicate timestamps + +v5.0.8 +- fix genius scraper +- add more accurate matching to genius scraper + +v5.0.7 +- add additional listitem properties for external use + +v5.0.6 +- fix focussed line selection +- more accurate time syncing + +v5.0.5 +- add support for internet streams + +v5.0.4 +- add lrc window property + +v5.0.3 +- fix embedded lyrics search + +v5.0.2 +- a bit more logging + +v5.0.1 +- fixed lyricwiki scraper + +v5.0.0 +- remove simplejson support +- update skin +- add another file naming template +- fix for Artist/Album/Track - title.ext +- also strip korean text + +v4.1.5 +- language update +- cleanup + +v4.1.4 +- fix detection of flac lyrics + +v4.1.3 +- fix error when clicking on a txt based lyric + +v4.1.2 +- update mutagen library + +v4.1.1 +- option to remove chinese text from lyrics +- fix only accept TXXX:lyrics tag + +v4.1.0 +- improve embedded mp3 lyrics support + +v4.0.2 +- added scraper for genius.com +- removed lyricstime scraper + +v4.0.1 +- update menu action code for jarvis + +v3.2.0 +- remove broken lrc scrapers +- fix unicodedecode crash in gomaudio + +v3.1.6 +- fix lyricwiki scraper + +v3.1.5 +- fix polish language file + +v3.1.4 +- add option to hide notifications + +v3.1.3 +- Update background media check to PlayingBackgroundMedia + +v3.1.2 +- Clean up the Monitor and Player classes on exit + +v3.1.1 +- Do not try and get lyrics if TvTunes is running + +v3.1.0 +- add support for mp4 files + +v3.0.11 +- properly handle lyrics in the uslt tag + +v3.0.10 +- add support for lrc lyrics inside the uslt tag + +v3.0.9 +- fix detection od osd key + +v3.0.8 +- allow codec info to be shown + +v3.0.7 +- better support for multiple artists + +v3.0.6 +- fixed missing string in language file + +v3.0.5 +- updated language files from Transifex + +v3.0.4 +- deprecate xbmc.abortRequested + +v3.0.3 +- re-label service setting + +v3.0.2 +- fix detection of text based Lyrics3 tags +- fix some lrc lyrics did not work (time tag not recognised) + +v3.0.1 +- fix lyricwiki scraper + +v3.0.0 +- kodi name change + +v2.0.10 +- several fixes + +v2.0.9 +- additional addon tags + +v2.0.8 +- add support for flac tags +- add do_not_analyze property for other addons + +v2.0.7 +- make music osd accesible +- show gui when user clicks osd button + +v2.0.6 +- add xml header + +v2.0.5 +- fix encoding issue + +v2.0.4 +- gotham release + +v2.0.3 +- gui cleanup + +v2.0.2 +- fix crash when failing to read lyrics file +- fixed update scraper list when settings change + +v2.0.1 +- fixed don't crash when trying to get embedded lyrics from online streams +- add option to clean song title + +v2.0.0 +- convert script to a service +- auto-hide window when no lyrics are found + +v1.0.7 +- fixed blank string in settings +- changed txxx field now supports both synchronised and regular lyrics +- fixed potential crash due to unhandled exceptions + +v1.0.6 +- fixed potential import of a third party scrapers module + +v1.0.5 +- fixed can't show lyric right after reselect in list, need reset control +- handle encode error in scraper GomAudio +- added option to save lyrics to song folder + +v1.0.4 +- fixed saving lyrics +- add Korean scraper(Alsong, GomAudio), credit for hojel + +v1.0.3 +- language update + +v1.0.2 +- add a script running window property +- added lyrics source as a window property +- make lyrics available as a window property +- fixed would fail for users with a special char in their username + +v1.0.1 +- fixed can't change lyric for track in cue/ape file +- fixed decode error in scrape minilyrics +- added requires for script.module.chardet + +v1.0.0 +- initial release + +v0.0.1 +- merge cu and lrc lyrics scripts + diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/audiofile.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/audiofile.py new file mode 100644 index 00000000000..b59ecadac60 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/audiofile.py @@ -0,0 +1,127 @@ +#-*- coding: UTF-8 -*- +""" +read audio stream from audio file +""" + +import os +import struct +import xbmcvfs + +class UnknownFormat(Exception):pass +class FormatError(Exception):pass + +class AudioFile(object): + f = None + audioStart = 0 + + def AudioFile(self): + self.f = None + self.audioStart = 0 + + def Open(self,filename): + self.audioStart = 0 + self.f = xbmcvfs.File(filename) + ext = os.path.splitext(filename)[1].lower() + if ext == '.mp3': self.AnalyzeMp3() + elif ext == '.ogg': self.AnalyzeOgg() + elif ext == '.wma': self.AnalyzeWma() + #elif ext == '.flac': self.AnalyzeFlac() + elif ext == '.flac': pass + elif ext == '.ape': pass + elif ext == '.wav': pass + else: # not supported format + self.f.close() + self.f = None + raise UnknownFormat + + def Close(self): + self.f.close() + self.f = None + + def ReadAudioStream(self, len, offset=0): + self.f.seek(self.audioStart+offset, 0) + return self.f.readBytes(len) + + def AnalyzeMp3(self): + # Searching ID3v2 tag + while True: + buf = self.f.readBytes(3) + if len(buf) < 3 or self.f.tell() > 50000: + # ID tag is not found + self.f.seek(0,0) + self.audioStart = 0 + return + if buf == b'ID3': + self.f.seek(3,1) # skip version/flag + # ID length (synchsafe integer) + tl = struct.unpack('4b', self.f.readBytes(4)) + taglen = (tl[0]<<21)|(tl[1]<<14)|(tl[2]<<7)|tl[3] + self.f.seek(taglen,1) + break + self.f.seek(-2,1) + # Searching MPEG SOF + while True: + buf = self.f.readBytes(1) + if len(buf) < 1 or self.f.seek(0,1) > 1000000: + raise FormatError + if buf == b'\xff': + rbit = struct.unpack('B',self.f.readBytes(1))[0] >> 5 + if rbit == 7: # 11 1's in total + self.f.seek(-2,1) + self.audioStart = self.f.tell() + return + + def AnalyzeOgg(self): + # Parse page (OggS) + while True: + buf = self.f.readBytes(27) # header + if len(buf) < 27 or self.f.tell() > 50000: + # parse error + raise FormatError + if buf[0:4] != b'OggS': + # not supported page format + raise UnknownFormat + numseg = struct.unpack('B', buf[26])[0] + #print "#seg: %d" % numseg + + segtbl = struct.unpack('%dB'%numseg, self.f.readBytes(numseg)) # segment table + for seglen in segtbl: + buf = self.f.readBytes(7) # segment header + #print "segLen(%s): %d" % (buf[1:7],seglen) + if buf == b"\x05vorbis": + self.f.seek(-7,1) # rollback + self.audioStart = self.f.tell() + return + self.f.seek(seglen-7,1) # skip to next segment + + def AnalyzeWma(self): + # Searching GUID + while True: + buf = self.f.readBytes(16) + if len(buf) < 16 or self.f.tell() > 50000: + raise FormatError + guid = buf.encode("hex"); + if guid == "3626b2758e66cf11a6d900aa0062ce6c": + # ASF_Data_Object + self.f.seek(-16,1) # rollback + self.audioStart = self.f.tell() + return + else: + objlen = struct.unpack(' 50000: + # not found + raise FormatError + metalen = buf[1] | (buf[2]<<8) | (buf[3]<<16); + self.f.seek(metalen,1) # skip this metadata block + if buf[0] & 0x80: + # it was the last metadata block + self.audioStart = self.f.tell() + return diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/lyricsScraper.py new file mode 100644 index 00000000000..456c04a5503 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/alsong/lyricsScraper.py @@ -0,0 +1,65 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for http://lyrics.alsong.co.kr/ +driip +''' + +import sys +import socket +import urllib.request +import difflib +import xml.dom.minidom as xml +from utilities import * + +__title__ = 'Alsong' +__priority__ = '150' +__lrc__ = True + +socket.setdefaulttimeout(10) + +ALSONG_URL = 'http://lyrics.alsong.net/alsongwebservice/service1.asmx' + +ALSONG_TMPL = '''\ + + + + + + %s + %s + 0 + + + + +''' + + +class LyricsFetcher: + def __init__(self): + self.base_url = 'http://lyrics.alsong.co.kr/' + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title)) + lyrics = Lyrics() + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + try: + headers = {'Content-Type':'text/xml; charset=utf-8'} + request = urllib.request.Request(ALSONG_URL, bytes(ALSONG_TMPL % (song.title,song.artist), 'utf-8'), headers) + response = urllib.request.urlopen(request) + Page = response.read().decode('utf-8') + except: + return + tree = xml.parseString(Page) + + try: + name = tree.getElementsByTagName('strArtistName')[0].childNodes[0].data + track = tree.getElementsByTagName('strTitle')[0].childNodes[0].data + except: + return + if (difflib.SequenceMatcher(None, song.artist.lower(), name.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, song.title.lower(), track.lower()).ratio() > 0.8): + lyr = tree.getElementsByTagName('strLyric')[0].childNodes[0].data.replace('
','\n') + lyrics.lyrics = lyr + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/lyricsScraper.py new file mode 100644 index 00000000000..37792457f55 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/baidu/lyricsScraper.py @@ -0,0 +1,51 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for http://www.baidu.com + +ronie +''' + +import urllib.request +import socket +import re +import chardet +import difflib +from utilities import * + +__title__ = 'Baidu' +__priority__ = '130' +__lrc__ = True + +socket.setdefaulttimeout(10) + +class LyricsFetcher: + def __init__(self): + self.BASE_URL = 'http://music.baidu.com/search/lrc?key=%s-%s' + self.LRC_URL = 'http://music.baidu.com%s' + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title)) + lyrics = Lyrics() + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + try: + url = self.BASE_URL % (song.title, song.artist) + data = urllib.request.urlopen(url).read().decode('utf-8') + songmatch = re.search('song-title.*?(.*?)', data, flags=re.DOTALL) + track = songmatch.group(1) + artistmatch = re.search('artist-title.*?(.*?)', data, flags=re.DOTALL) + name = artistmatch.group(1) + urlmatch = re.search("down-lrc-btn.*?':'(.*?)'", data, flags=re.DOTALL) + found_url = urlmatch.group(1) + if (difflib.SequenceMatcher(None, song.artist.lower(), name.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, song.title.lower(), track.lower()).ratio() > 0.8): + lyr = urllib.request.urlopen(self.LRC_URL % found_url).read() + else: + return + except: + return + + enc = chardet.detect(lyr) + lyr = lyr.decode(enc['encoding'], 'ignore') + lyrics.lyrics = lyr + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/lyricsScraper.py new file mode 100644 index 00000000000..1700d232e16 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/gomaudio/lyricsScraper.py @@ -0,0 +1,101 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for http://newlyrics.gomtv.com/ + +edge +''' + +import sys +import hashlib +import requests +import urllib.parse +import re +import unicodedata +from lib.utils import * +from lib.audiofile import AudioFile + +__title__ = 'GomAudio' +__priority__ = '110' +__lrc__ = True + + +GOM_URL = 'http://newlyrics.gomtv.com/cgi-bin/lyrics.cgi?cmd=find_get_lyrics&file_key=%s&title=%s&artist=%s&from=gomaudio_local' + +def remove_accents(data): + nfkd_data = unicodedata.normalize('NFKD', data) + return u"".join([c for c in nfkd_data if not unicodedata.combining(c)]) + + +class gomClient(object): + ''' + privide Gom specific function, such as key from mp3 + ''' + @staticmethod + def GetKeyFromFile(file): + musf = AudioFile() + musf.Open(file) + buf = musf.ReadAudioStream(100*1024) # 100KB from audio data + musf.Close() + # buffer will be empty for streaming audio + if not buf: + return + # calculate hashkey + m = hashlib.md5() + m.update(buf) + return m.hexdigest() + + @staticmethod + def mSecConv(msec): + s,ms = divmod(msec/10,100) + m,s = divmod(s,60) + return m,s,ms + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.base_url = 'http://newlyrics.gomtv.com/' + + def get_lyrics(self, song, key=None, ext=None): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + try: + if not ext: + ext = os.path.splitext(song.filepath)[1].lower() + sup_ext = ['.mp3', '.ogg', '.wma', '.flac', '.ape', '.wav'] + if ext in sup_ext and key == None: + key = gomClient.GetKeyFromFile(song.filepath) + if not key: + return None + url = GOM_URL %(key, urllib.parse.quote(remove_accents(song.title).encode('euc-kr')), urllib.parse.quote(remove_accents(song.artist).encode('euc-kr'))) + response = requests.get(url, timeout=10) + response.encoding = 'euc-kr' + Page = response.text + except: + log('%s: %s::%s (%d) [%s]' % ( + __title__, self.__class__.__name__, + sys.exc_info()[2].tb_frame.f_code.co_name, + sys.exc_info()[2].tb_lineno, + sys.exc_info()[1] + ), debug=self.DEBUG) + return None + if Page[:Page.find('>')+1] != '': + return None + syncs = re.compile('([^<]*)').findall(Page) + lyrline = [] + lyrline.append('[ti:%s]' %song.title) + lyrline.append('[ar:%s]' %song.artist) + for sync in syncs: + # timeformat conversion + t = '%02d:%02d.%02d' % gomClient.mSecConv(int(sync[0])) + # unescape string + try: + s = sync[1].replace(''',"'").replace('"','"') + lyrline.append('[%s]%s' %(t,s)) + except: + pass + lyrics.lyrics = '\n'.join(lyrline) + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/lyricsScraper.py new file mode 100644 index 00000000000..5291abc9f3f --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricwiki/lyricsScraper.py @@ -0,0 +1,67 @@ +#-*- coding: UTF-8 -*- +import sys +import re +import json +import requests +from urllib.error import HTTPError +import urllib.parse +from html.parser import HTMLParser +import xbmc +import xbmcaddon +from lib.utils import * + +__title__ = 'lyricwiki' +__priority__ = '200' +__lrc__ = False + +LIC_TXT = 'we are not licensed to display the full lyrics for this song at the moment' + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.url = 'http://lyrics.wikia.com/api.php?func=getSong&artist=%s&song=%s&fmt=realjson' + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + try: + req = requests.get(self.url % (urllib.parse.quote(song.artist), urllib.parse.quote(song.title)), timeout=10) + response = req.text + except: + return None + data = json.loads(response) + try: + self.page = data['url'] + except: + return None + if not self.page.endswith('action=edit'): + log('%s: search url: %s' % (__title__, self.page), debug=self.DEBUG) + try: + req = requests.get(self.page, timeout=10) + response = req.text + except requests.exceptions.HTTPError as error: # strange... sometimes lyrics are returned with a 404 error + if error.response.status_code == 404: + response = error.response.text + else: + return None + except: + return None + matchcode = re.search("class='lyricbox'>(.*?)', '\n') + lyr = re.sub('<[^<]+?>', '', lyricstext) + if LIC_TXT in lyr: + return None + lyrics.lyrics = lyr + return lyrics + except: + return None + else: + return None diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/lyricsScraper.py new file mode 100644 index 00000000000..3bed05a2fa2 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/minilyrics/lyricsScraper.py @@ -0,0 +1,161 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for http://www.viewlyrics.com + +PedroHLC +https://github.com/PedroHLC/ViewLyricsOpenSearcher + +rikels +https://github.com/rikels/LyricsSearch +''' + +import re +import hashlib +import difflib +import chardet +import requests +from lib.utils import * + +__title__ = 'MiniLyrics' +__priority__ = '100' +__lrc__ = True + + +class MiniLyrics(object): + ''' + Minilyrics specific functions + ''' + @staticmethod + def hexToStr(hexx): + string = '' + i = 0 + while (i < (len(hexx) - 1)): + string += chr(int(hexx[i] + hexx[i + 1], 16)) + i += 2 + return string + + @staticmethod + def vl_enc(data, md5_extra): + datalen = len(data) + md5 = hashlib.md5() + md5.update(data + md5_extra) + hasheddata = MiniLyrics.hexToStr(md5.hexdigest()) + j = 0 + i = 0 + while (i < datalen): + try: + j += data[i] + except TypeError: + j += ord(data[i]) + i += 1 + magickey = chr(int(round(float(j) / float(datalen)))) + encddata = list(range(len(data))) + if isinstance(magickey, int): + pass + else: + magickey = ord(magickey) + for i in range(datalen): + if isinstance(data[i], int): + encddata[i] = data[i] ^ magickey + else: + encddata[i] = ord(data[i]) ^ magickey + try: + result = '\x02' + chr(magickey) + '\x04\x00\x00\x00' + str(hasheddata) + bytearray(encddata).decode('utf-8') + except UnicodeDecodeError: + ecd = chardet.detect(bytearray(encddata)) + if ecd['encoding']: + try: + result = '\x02' + chr(magickey) + '\x04\x00\x00\x00' + str(hasheddata) + bytearray(encddata).decode(ecd['encoding']) + except: + result = '\x02' + chr(magickey) + '\x04\x00\x00\x00' + str(hasheddata) + "".join(map(chr, bytearray(encddata))) + else: + result = '\x02' + chr(magickey) + '\x04\x00\x00\x00' + str(hasheddata) + "".join(map(chr, bytearray(encddata))) + return result + + @staticmethod + def vl_dec(data): + magickey = data[1] + result = "" + i = 22 + datalen = len(data) + if isinstance(magickey, int): + pass + else: + magickey = ord(magickey) + for i in range(22, datalen): + if isinstance(data[i], int): + result += chr(data[i] ^ magickey) + else: + result += chr(ord(data[i]) ^ magickey) + return result + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.proxy = None + + def htmlDecode(self,string): + entities = {''':'\'','"':'"','>':'>','<':'<','&':'&'} + for i in entities: + string = string.replace(i,entities[i]) + return string + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + search_url = 'http://search.crintsoft.com/searchlyrics.htm' + search_query_base = "" + search_useragent = 'MiniLyrics' + search_md5watermark = b'Mlv1clt4.0' + search_encquery = MiniLyrics.vl_enc(search_query_base.format(artist=song.artist, title=song.title).encode('utf-8'), search_md5watermark) + headers = {"User-Agent": "{ua}".format(ua=search_useragent), + "Content-Length": "{content_length}".format(content_length=len(search_encquery)), + "Connection": "Keep-Alive", + "Expect": "100-continue", + "Content-Type": "application/x-www-form-urlencoded" + } + try: + request = requests.post(search_url, data=search_encquery, headers=headers, timeout=10) + search_result = request.text + except: + return + rawdata = MiniLyrics.vl_dec(search_result) + # might be a better way to parse the data + lrcdata = rawdata.replace('\x00', '*') + artistmatch = re.search('artist\*(.*?)\*',lrcdata) + if not artistmatch: + return + titlematch = re.search('title\*(.*?)\*',lrcdata) + if not titlematch: + return + artist = artistmatch.group(1) + title = titlematch.group(1) + links = [] + if (difflib.SequenceMatcher(None, song.artist.lower(), artist.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, song.title.lower(), title.lower()).ratio() > 0.8): + results = re.findall('[a-z0-9/_]*?\.lrc', lrcdata) + for item in results: + links.append((artist + ' - ' + title, item, artist, title)) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + lyr = self.get_lyrics_from_list(links[0]) + if not lyr: + return None + lyrics.lyrics = lyr + return lyrics + + def get_lyrics_from_list(self, link): + title,url,artist,song = link + try: + f = requests.get('http://search.crintsoft.com/l/' + url, timeout=10) + lyrics = f.content + except: + return + enc = chardet.detect(lyrics) + lyrics = lyrics.decode(enc['encoding'], 'ignore') + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/lyricsScraper.py new file mode 100644 index 00000000000..15f108347cc --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/ttplayer/lyricsScraper.py @@ -0,0 +1,207 @@ +#-*- coding: UTF-8 -*- +""" +Scraper for http://lrcct2.ttplayer.com/ + +taxigps +""" + +import os +import socket +import urllib.request +import re +import random +import difflib +from lib.utils import * + +__title__ = "TTPlayer" +__priority__ = '110' +__lrc__ = True + +UserAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0' + +socket.setdefaulttimeout(10) + +LYRIC_TITLE_STRIP=["\(live[^\)]*\)", "\(acoustic[^\)]*\)", + "\([^\)]*mix\)", "\([^\)]*version\)", + "\([^\)]*edit\)", "\(feat[^\)]*\)"] +LYRIC_TITLE_REPLACE=[("/", "-"),(" & ", " and ")] +LYRIC_ARTIST_REPLACE=[("/", "-"),(" & ", " and ")] + +class ttpClient(object): + ''' + privide ttplayer specific function, such as encoding artist and title, + generate a Id code for server authorizition. + (see http://ttplyrics.googlecode.com/svn/trunk/crack) + ''' + @staticmethod + def CodeFunc(Id, data): + ''' + Generate a Id Code + These code may be ugly coz it is translated + from C code which is translated from asm code + grabed by ollydbg from ttp_lrcs.dll. + (see http://ttplyrics.googlecode.com/svn/trunk/crack) + ''' + length = len(data) + + tmp2=0 + tmp3=0 + + tmp1 = (Id & 0x0000FF00) >> 8 #右移8位后为x0000015F + + #tmp1 0x0000005F + if ((Id & 0x00FF0000) == 0): + tmp3 = 0x000000FF & ~tmp1 #CL 0x000000E7 + else: + tmp3 = 0x000000FF & ((Id & 0x00FF0000) >> 16) #右移16后为x00000001 + + tmp3 = tmp3 | ((0x000000FF & Id) << 8) #tmp3 0x00001801 + tmp3 = tmp3 << 8 #tmp3 0x00180100 + tmp3 = tmp3 | (0x000000FF & tmp1) #tmp3 0x0018015F + tmp3 = tmp3 << 8 #tmp3 0x18015F00 + if ((Id & 0xFF000000) == 0) : + tmp3 = tmp3 | (0x000000FF & (~Id)) #tmp3 0x18015FE7 + else : + tmp3 = tmp3 | (0x000000FF & (Id >> 24)) #右移24位后为0x00000000 + + #tmp3 18015FE7 + + i=length-1 + while(i >= 0): + char = ord(data[i]) + if char >= 0x80: + char = char - 0x100 + tmp1 = (char + tmp2) & 0x00000000FFFFFFFF + tmp2 = (tmp2 << (i%2 + 4)) & 0x00000000FFFFFFFF + tmp2 = (tmp1 + tmp2) & 0x00000000FFFFFFFF + #tmp2 = (ord(data[i])) + tmp2 + ((tmp2 << (i%2 + 4)) & 0x00000000FFFFFFFF) + i -= 1 + + #tmp2 88203cc2 + i=0 + tmp1=0 + while(i<=length-1): + char = ord(data[i]) + if char >= 128: + char = char - 256 + tmp7 = (char + tmp1) & 0x00000000FFFFFFFF + tmp1 = (tmp1 << (i%2 + 3)) & 0x00000000FFFFFFFF + tmp1 = (tmp1 + tmp7) & 0x00000000FFFFFFFF + #tmp1 = (ord(data[i])) + tmp1 + ((tmp1 << (i%2 + 3)) & 0x00000000FFFFFFFF) + i += 1 + + #EBX 5CC0B3BA + + #EDX = EBX | Id + #EBX = EBX | tmp3 + tmp1 = (((((tmp2 ^ tmp3) & 0x00000000FFFFFFFF) + (tmp1 | Id)) & 0x00000000FFFFFFFF) * (tmp1 | tmp3)) & 0x00000000FFFFFFFF + tmp1 = (tmp1 * (tmp2 ^ Id)) & 0x00000000FFFFFFFF + + if tmp1 > 0x80000000: + tmp1 = tmp1 - 0x100000000 + return tmp1 + + @staticmethod + def EncodeArtTit(data): + data = data.encode('UTF-16').decode('UTF-16') + rtn = '' + for i in range(len(data)): + rtn += '%02x00' % ord(data[i]) + return rtn + + +class LyricsFetcher: + def __init__(self): + self.LIST_URL = 'http://ttlrccnc.qianqian.com/dll/lyricsvr.dll?sh?Artist=%s&Title=%s&Flags=0' + self.LYRIC_URL = 'http://ttlrccnc.qianqian.com/dll/lyricsvr.dll?dl?Id=%d&Code=%d&uid=01&mac=%012x' + + def get_lyrics(self, song): + log("%s: searching lyrics for %s - %s" % (__title__, song.artist, song.title)) + lyrics = Lyrics() + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = song.artist + title = song.title + # replace ampersands and the like + for exp in LYRIC_ARTIST_REPLACE: + p = re.compile(exp[0]) + artist = p.sub(exp[1], artist) + for exp in LYRIC_TITLE_REPLACE: + p = re.compile(exp[0]) + title = p.sub(exp[1], title) + + # strip things like "(live at Somewhere)", "(accoustic)", etc + for exp in LYRIC_TITLE_STRIP: + p = re.compile(exp) + title = p.sub('', title) + + # compress spaces + title = title.strip().replace('`','').replace('/','') + artist = artist.strip().replace('`','').replace('/','') + + try: + url = self.LIST_URL %(ttpClient.EncodeArtTit(artist.replace(' ','').lower()), ttpClient.EncodeArtTit(title.replace(' ','').lower())) + f = urllib.request.urlopen(url) + Page = f.read().decode('utf-8') + except: + log("%s: %s::%s (%d) [%s]" % ( + __title__, self.__class__.__name__, + sys.exc_info()[2].tb_frame.f_code.co_name, + sys.exc_info()[2].tb_lineno, + sys.exc_info()[1] + )) + return None + links_query = re.compile('') + urls = re.findall(links_query, Page) + links = [] + for x in urls: + if (difflib.SequenceMatcher(None, artist.lower(), x[1].lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, title.lower(), x[2].lower()).ratio() > 0.8): + links.append((x[1] + ' - ' + x[2], x[0], x[1], x[2])) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + for link in links: + lyr = self.get_lyrics_from_list(link) + if lyr and lyr.startswith('['): + lyrics.lyrics = lyr + return lyrics + return None + + def get_lyrics_from_list(self, link): + title,Id,artist,song = link + try: + + url = self.LYRIC_URL %(int(Id),ttpClient.CodeFunc(int(Id), artist + song), random.randint(0,0xFFFFFFFFFFFF)) + log('%s: search url: %s' % (__title__, url)) + header = {'User-Agent':UserAgent} + req = urllib.request.Request(url, headers=header) + f = urllib.request.urlopen(req) + Page = f.read().decode('utf-8') + except: + log("%s: %s::%s (%d) [%s]" % ( + __title__, self.__class__.__name__, + sys.exc_info()[2].tb_frame.f_code.co_name, + sys.exc_info()[2].tb_lineno, + sys.exc_info()[1] + )) + return None + # ttplayer occasionally returns incorrect lyrics. if we have a 'ti' and/or an 'ar' tag with a value we can check if they match the title and artist + if Page.startswith('[ti:'): + check = Page.split('\n') + if not check[0][4:-1] == '': + if (difflib.SequenceMatcher(None, song.lower(), check[0][4:-1].lower()).ratio() > 0.8): + return Page + else: + return '' + if check[1][0:4] == '[ar:' and not check[1][4:-1] == '': + if (difflib.SequenceMatcher(None, artist.lower(), check[1][4:-1].lower()).ratio() > 0.8): + return Page + else: + return '' + else: + return Page + elif Page.startswith('['): + return Page + return '' diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/lyricsScraper.py new file mode 100644 index 00000000000..b850ac4938d --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/xiami/lyricsScraper.py @@ -0,0 +1,96 @@ +#-*- coding: UTF-8 -*- +""" +Scraper for https://xiami.com + +Taxigps +""" + +import urllib.parse +import socket +import re +import difflib +import json +import chardet +import requests +from utilities import * + +__title__ = "Xiami" +__priority__ = '110' +__lrc__ = True + +UserAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0' + +socket.setdefaulttimeout(10) + +class LyricsFetcher: + def __init__( self ): + self.LIST_URL = 'https://www.xiami.com/search?key=%s' + self.SONG_URL = 'https://www.xiami.com/song/playlist/id/%s/object_name/default/object_id/0' + self.session = requests.Session() + + def get_lyrics(self, song): + log( "%s: searching lyrics for %s - %s" % (__title__, song.artist, song.title)) + lyrics = Lyrics() + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + keyword = "%s %s" % (song.title, song.artist) + url = self.LIST_URL % (urllib.parse.quote(keyword)) + try: + response = self.session.get(url, headers={'User-Agent': UserAgent, 'Referer': 'https://www.xiami.com/play'}) + result = response.text + except: + log( "%s: %s::%s (%d) [%s]" % ( + __title__, self.__class__.__name__, + sys.exc_info()[ 2 ].tb_frame.f_code.co_name, + sys.exc_info()[ 2 ].tb_lineno, + sys.exc_info()[ 1 ] + )) + return None + match = re.compile('.+?value="(.+?)".+?href="//www.xiami.com/song/[^"]+" title="([^"]+)".*?href="//www.xiami.com/artist/[^"]+" title="([^"]+)"', re.DOTALL).findall(result) + links = [] + for x in match: + title = x[1] + artist = x[2] + if (difflib.SequenceMatcher(None, song.artist.lower(), artist.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, song.title.lower(), title.lower()).ratio() > 0.8): + links.append( ( artist + ' - ' + title, x[0], artist, title ) ) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + lyr = self.get_lyrics_from_list(links[0]) + if not lyr: + return None + lyrics.lyrics = lyr + return lyrics + + def get_lyrics_from_list(self, link): + title,id,artist,song = link + try: + response = self.session.get(self.SONG_URL % (id), headers={'User-Agent': UserAgent, 'Referer': 'https://www.xiami.com/play'}) + result = response.text + data = json.loads(result) + if 'data' in data and 'trackList' in data['data'] and data['data']['trackList'] and 'lyric' in data['data']['trackList'][0] and data['data']['trackList'][0]['lyric']: + url = data['data']['trackList'][0]['lyric'] + except: + log( "%s: %s::%s (%d) [%s]" % ( + __title__, self.__class__.__name__, + sys.exc_info()[ 2 ].tb_frame.f_code.co_name, + sys.exc_info()[ 2 ].tb_lineno, + sys.exc_info()[ 1 ] + )) + return + try: + response = self.session.get(url, headers={'User-Agent': UserAgent, 'Referer': 'https://www.xiami.com/play'}) + lyrics = response.content + except: + log( "%s: %s::%s (%d) [%s]" % ( + __title__, self.__class__.__name__, + sys.exc_info()[ 2 ].tb_frame.f_code.co_name, + sys.exc_info()[ 2 ].tb_lineno, + sys.exc_info()[ 1 ] + )) + return + enc = chardet.detect(lyrics) + lyrics = lyrics.decode(enc['encoding'], 'ignore') + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/lyricsScraper.py new file mode 100644 index 00000000000..7fcf294f023 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/azlyrics/lyricsScraper.py @@ -0,0 +1,42 @@ +#-*- coding: UTF-8 -*- +import sys +import re +import requests +import html +import xbmc +import xbmcaddon +from lib.utils import * + +__title__ = 'azlyrics' +__priority__ = '230' +__lrc__ = False + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.url = 'https://www.azlyrics.com/lyrics/%s/%s.html' + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = re.sub("[^a-zA-Z0-9]+", "", song.artist).lower().lstrip('the ') + title = re.sub("[^a-zA-Z0-9]+", "", song.title).lower() + try: + req = requests.get(self.url % (artist, title), timeout=10) + response = req.text + except: + return None + req.close() + try: + lyricscode = response.split('t. -->')[1].split('', '\n') + lyr = re.sub('<[^<]+?>', '', lyricstext) + lyrics.lyrics = lyr + return lyrics + except: + return None diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/lyricsScraper.py new file mode 100644 index 00000000000..d24a32bd6fd --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/darklyrics/lyricsScraper.py @@ -0,0 +1,124 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for http://www.darklyrics.com/ - the largest metal lyrics archive on the Web. + +scraper by smory +''' + +import hashlib +import math +import requests +import time +import urllib.parse +import re +from lib.utils import * +try: + from ctypes import c_int32 # ctypes not supported on xbox +except: + pass + +__title__ = 'darklyrics' +__priority__ = '260' +__lrc__ = False + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.base_url = 'http://www.darklyrics.com/' + self.searchUrl = 'http://www.darklyrics.com/search?q=%s' + self.cookie = self.getCookie() + + def getCookie(self): + # http://www.darklyrics.com/tban.js + lastvisitts = 'Nergal' + str(math.ceil(time.time() * 1000 / (60 * 60 * 6 * 1000))) + lastvisittscookie = 0 + i = 0 + while i < len(lastvisitts): + try: + lastvisittscookie = c_int32((c_int32(lastvisittscookie<<5).value - c_int32(lastvisittscookie).value) + ord(lastvisitts[i])).value + except: + return + i += 1 + lastvisittscookie = lastvisittscookie & lastvisittscookie + return str(lastvisittscookie) + + def search(self, artist, title): + term = urllib.parse.quote((artist if artist else '') + '+' + (title if title else '')) + try: + headers = {'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'} + req = requests.get(self.searchUrl % term, headers=headers, cookies={'lastvisitts': self.cookie}, timeout=10) + searchResponse = req.text + except: + return None + searchResult = re.findall('

(.*?)

', searchResponse) + if len(searchResult) == 0: + return None + links = [] + i = 0 + for result in searchResult: + a = [] + a.append(result[2] + (' ' + self.getAlbumName(self.base_url + result[0]) if i < 6 else '')) # title from server + album nane + a.append(self.base_url + result[0]) # url with lyrics + a.append(artist) + a.append(title) + a.append(result[1]) # id of the side part containing this song lyrics + links.append(a) + i += 1 + return links + + def findLyrics(self, url, index): + try: + headers = {'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'} + req = requests.get(url, headers=headers, cookies={'lastvisitts': self.cookie}, timeout=10) + res = req.text + except: + return None + pattern = '(.*?)(?:

|', '') + s = s.replace('', '') + s = s.replace('', '') + s = s.replace('', '') + s = s.replace('

', '') + return s + else: + return None + + def getAlbumName(self, url): + try: + headers = {'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'} + req = requests.get(url, headers=headers, cookies={'lastvisitts': self.cookie}, timeout=10) + res = req.text + except: + return '' + match = re.search('

(?:album|single|ep|live):?\s?(.*?)

', res, re.IGNORECASE) + if match: + return ('(' + match.group(1) + ')').replace('\'', '') + else: + return '' + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + links = self.search(song.artist , song.title) + if(links == None or len(links) == 0): + return None + elif len(links) > 1: + lyrics.list = links + lyr = self.get_lyrics_from_list(links[0]) + if not lyr: + return None + lyrics.lyrics = lyr + return lyrics + + def get_lyrics_from_list(self, link): + title, url, artist, song, index = link + return self.findLyrics(url, index) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/lyricsScraper.py new file mode 100644 index 00000000000..7e8ffcd9023 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/genius/lyricsScraper.py @@ -0,0 +1,68 @@ +#-*- coding: UTF-8 -*- +import sys +import re +import urllib.parse +import requests +import html +import xbmc +import xbmcaddon +import json +import difflib +from lib.utils import * + +__title__ = 'genius' +__priority__ = '200' +__lrc__ = False + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.url = 'http://api.genius.com/search?q=%s%%20%s&access_token=Rq_cyNZ6fUOQr4vhyES6vu1iw3e94RX85ju7S8-0jhM-gftzEvQPG7LJrrnTji11' + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + try: + headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; rv:77.0) Gecko/20100101 Firefox/77.0'} + url = self.url % (urllib.parse.quote(song.artist), urllib.parse.quote(song.title)) + req = requests.get(url, headers=headers, timeout=10) + response = req.text + except: + return None + data = json.loads(response) + try: + name = data['response']['hits'][0]['result']['primary_artist']['name'] + track = data['response']['hits'][0]['result']['title'] + if (difflib.SequenceMatcher(None, song.artist.lower(), name.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, song.title.lower(), track.lower()).ratio() > 0.8): + self.page = data['response']['hits'][0]['result']['url'] + else: + return None + except: + return None + log('%s: search url: %s' % (__title__, self.page), debug=self.DEBUG) + try: + headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; rv:77.0) Gecko/20100101 Firefox/77.0'} + req = requests.get(self.page, headers=headers, timeout=10) + response = req.text + except: + return None + response = html.unescape(response) + matchcode = re.findall('class="Lyrics__Container.*?">(.*?)', '\n', lyricscode) + lyr2 = re.sub('<[^<]+?>', '', lyr1) + lyr3 = lyr2.replace('\\n','\n').strip() + if not lyr3 or lyr3 == '[Instrumental]' or lyr3.startswith('Lyrics for this song have yet to be released'): + return None + lyrics.lyrics = lyr3 + return lyrics + except: + return None diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/lyricsScraper.py new file mode 100644 index 00000000000..5f45834476e --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lrclib/lyricsScraper.py @@ -0,0 +1,66 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for https://lrclib.net/ + +lrclib + +https://github.com/rtcq/syncedlyrics +''' + +import requests +import difflib +from lib.utils import * + +__title__ = "lrclib" +__priority__ = '110' +__lrc__ = True + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.SEARCH_URL = 'https://lrclib.net/api/search?q=%s-%s' + self.LYRIC_URL = 'https://lrclib.net/api/get/%i' + + def get_lyrics(self, song): + log("%s: searching lyrics for %s - %s" % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + try: + url = self.SEARCH_URL % (song.artist, song.title) + response = requests.get(url, timeout=10) + result = response.json() + except: + return None + links = [] + for item in result: + artistname = item['artistName'] + songtitle = item['name'] + songid = item['id'] + if (difflib.SequenceMatcher(None, song.artist.lower(), artistname.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, song.title.lower(), songtitle.lower()).ratio() > 0.8): + links.append((artistname + ' - ' + songtitle, self.LYRIC_URL % songid, artistname, songtitle)) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + for link in links: + lyr = self.get_lyrics_from_list(link) + if lyr: + lyrics.lyrics = lyr + return lyrics + return None + + def get_lyrics_from_list(self, link): + title,url,artist,song = link + try: + log('%s: search url: %s' % (__title__, url), debug=self.DEBUG) + response = requests.get(url, timeout=10) + result = response.json() + except: + return None + if 'syncedLyrics' in result: + lyrics = result['syncedLyrics'] + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/lyricsScraper.py new file mode 100644 index 00000000000..a04720c1cce --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/lyricsScraper.py @@ -0,0 +1,61 @@ +#-*- coding: UTF-8 -*- +import re +import requests +import urllib.parse +import difflib +from bs4 import BeautifulSoup +from lib.utils import * + +__title__ = 'lyricscom' +__priority__ = '240' +__lrc__ = False + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.url = 'https://www.lyrics.com/serp.php?st=%s&qtype=2' + + def get_lyrics(self, song): + sess = requests.Session() + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + try: + request = sess.get(self.url % urllib.parse.quote_plus(song.artist), timeout=10) + response = request.text + except: + return + soup = BeautifulSoup(response, 'html.parser') + url = '' + for link in soup.find_all('a'): + if link.string and link.get('href').startswith('artist/'): + url = 'https://www.lyrics.com/' + link.get('href') + break + if url: + try: + req = sess.get(url, timeout=10) + resp = req.text + except: + return + soup = BeautifulSoup(resp, 'html.parser') + url = '' + for link in soup.find_all('a'): + if link.string and (difflib.SequenceMatcher(None, link.string.lower(), song.title.lower()).ratio() > 0.8): + url = 'https://www.lyrics.com' + link.get('href') + break + if url: + try: + req2 = sess.get(url, timeout=10) + resp2 = req2.text + except: + return + matchcode = re.search('(.*?)', resp2, flags=re.DOTALL) + if matchcode: + lyricscode = (matchcode.group(1)) + lyr = re.sub('<[^<]+?>', '', lyricscode) + lyrics.lyrics = lyr.replace('\\n','\n') + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py new file mode 100644 index 00000000000..dba13e3dd9e --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py @@ -0,0 +1,75 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for https://www.lyricsify.com/ +''' + +import requests +import re +import difflib +from bs4 import BeautifulSoup +from lib.utils import * + +__title__ = "Lyricsify" +__priority__ = '130' +__lrc__ = True + +UserAgent = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"} + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.SEARCH_URL = 'https://www.lyricsify.com/lyrics/%s/%s' + self.LYRIC_URL = 'https://www.lyricsify.com%s' + + def get_lyrics(self, song): + log("%s: searching lyrics for %s - %s" % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = song.artist.replace(' ', '-') + title = song.title.replace(' ', '-') + try: + url = self.SEARCH_URL % (artist, title) + search = requests.get(url, headers=UserAgent, timeout=10) + response = search.text + except: + return None + links = [] + soup = BeautifulSoup(response, 'html.parser') + for link in soup.find_all('a'): + if link.string and link.get('href').startswith('/lrc/'): + foundartist = link.string.split(' - ', 1)[0] + # some links don't have a proper 'artist - title' format + try: + foundsong = link.string.split(' - ', 1)[1].rstrip('.lrc') + except: + continue + if (difflib.SequenceMatcher(None, artist.lower(), foundartist.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, title.lower(), foundsong.lower()).ratio() > 0.8): + links.append((foundartist + ' - ' + foundsong, self.LYRIC_URL % link.get('href'), foundartist, foundsong)) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + for link in links: + lyr = self.get_lyrics_from_list(link) + if lyr: + lyrics.lyrics = lyr + return lyrics + return None + + def get_lyrics_from_list(self, link): + title,url,artist,song = link + try: + log('%s: search url: %s' % (__title__, url), debug=self.DEBUG) + search = requests.get(url, headers=UserAgent, timeout=10) + response = search.text + except: + return None + matchcode = re.search('/h3>(.*?)', '', lyricscode) + return cleanlyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/lyricsScraper.py new file mode 100644 index 00000000000..2b57acebfb3 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsmode/lyricsScraper.py @@ -0,0 +1,59 @@ +#-*- coding: UTF-8 -*- +import sys +import requests +import urllib.parse +import re +from lib.utils import * + +__title__ = 'lyricsmode' +__priority__ = '220' +__lrc__ = False + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = deAccent(song.artist) + title = deAccent(song.title) + url = 'http://www.lyricsmode.com/lyrics/%s/%s/%s.html' % (artist.lower()[:1], artist.lower().replace('&','and').replace(' ','_'), title.lower().replace('&','and').replace(' ','_')) + result = self.direct_url(url) + if not result: + result = self.search_url(artist, title) + if result: + lyr = result.split('style="position: relative;">')[1].split('', '') + return lyrics + + def direct_url(self, url): + try: + log('%s: direct url: %s' % (__title__, url), debug=self.DEBUG) + song_search = requests.get(url, timeout=10) + response = song_search.text + if response.find('lyrics_text') >= 0: + return response + except: + log('error in direct url', debug=self.DEBUG) + + def search_url(self, artist, title): + try: + url = 'http://www.lyricsmode.com/search.php?search=' + urllib.parse.quote_plus(artist.lower() + ' ' + title.lower()) + log('%s: search url: %s' % (__title__, url), debug=self.DEBUG) + song_search = requests.get(url, timeout=10) + response = song_search.text + matchcode = re.search('lm-list__cell-title">.*? 1: + lyrics.list = links + for link in links: + lyr = self.get_lyrics_from_list(link) + if lyr: + lyrics.lyrics = lyr + return lyrics + return None + + def get_lyrics_from_list(self, link): + title,url,artist,song = link + try: + log('%s: search url: %s' % (__title__, url), debug=self.DEBUG) + response = requests.get(url, timeout=10) + result = response.text + except: + return None + matchcode = re.search('span id="lrc_[0-9]+_lyrics">(.*?)', '', lyricscode) + return cleanlyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/lyricsScraper.py new file mode 100644 index 00000000000..3f546399f75 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/music163/lyricsScraper.py @@ -0,0 +1,71 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for http://music.163.com/ + +osdlyrics +''' + +import os +import requests +import re +import random +import difflib +from lib.utils import * + +__title__ = "Music163" +__priority__ = '120' +__lrc__ = True + +headers = {} +headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0' + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.SEARCH_URL = 'http://music.163.com/api/search/get' + self.LYRIC_URL = 'http://music.163.com/api/song/lyric' + + def get_lyrics(self, song): + log("%s: searching lyrics for %s - %s" % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = song.artist.replace(' ', '+') + title = song.title.replace(' ', '+') + search = '?s=%s+%s&type=1' % (artist, title) + try: + url = self.SEARCH_URL + search + response = requests.get(url, headers=headers, timeout=10) + result = response.json() + except: + return None + links = [] + if 'result' in result and 'songs' in result['result']: + for item in result['result']['songs']: + artists = "+&+".join([a["name"] for a in item["artists"]]) + if (difflib.SequenceMatcher(None, artist.lower(), artists.lower()).ratio() > 0.6) and (difflib.SequenceMatcher(None, title.lower(), item['name'].lower()).ratio() > 0.8): + links.append((artists + ' - ' + item['name'], self.LYRIC_URL + '?id=' + str(item['id']) + '&lv=-1&kv=-1&tv=-1', artists, item['name'])) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + for link in links: + lyr = self.get_lyrics_from_list(link) + if lyr and lyr.startswith('['): + lyrics.lyrics = lyr + return lyrics + return None + + def get_lyrics_from_list(self, link): + title,url,artist,song = link + try: + log('%s: search url: %s' % (__title__, url), debug=self.DEBUG) + response = requests.get(url, headers=headers, timeout=10) + result = response.json() + except: + return None + if 'lrc' in result: + return result['lrc']['lyric'] diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/lyricsScraper.py new file mode 100644 index 00000000000..60e6e36b8fb --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/lyricsScraper.py @@ -0,0 +1,86 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for https://www.musixmatch.com/ + +musixmatch +''' + +import os +import requests +import re +import random +import difflib +from bs4 import BeautifulSoup +from lib.utils import * + +__title__ = "musixmatch" +__priority__ = '210' +__lrc__ = False + +headers = {} +headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0' + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.SEARCH_URL = 'https://www.musixmatch.com/search/' + self.LYRIC_URL = 'https://www.musixmatch.com' + + def get_lyrics(self, song): + log("%s: searching lyrics for %s - %s" % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = song.artist.replace(' ', '+') + title = song.title.replace(' ', '+') + search = '%s+%s' % (artist, title) + try: + url = self.SEARCH_URL + search + response = requests.get(url, headers=headers, timeout=10) + result = response.text + except: + return None + links = [] + soup = BeautifulSoup(result, 'html.parser') + for item in soup.find_all('li', {'class': 'showArtist'}): + artistname = item.find('a', {'class': 'artist'}).get_text() + songtitle = item.find('a', {'class': 'title'}).get_text() + url = item.find('a', {'class': 'title'}).get('href') + if (difflib.SequenceMatcher(None, artist.lower(), artistname.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, title.lower(), songtitle.lower()).ratio() > 0.8): + links.append((artistname + ' - ' + songtitle, self.LYRIC_URL + url, artistname, songtitle)) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + for link in links: + lyr = self.get_lyrics_from_list(link) + if lyr: + lyrics.lyrics = lyr + return lyrics + return None + + def get_lyrics_from_list(self, link): + title,url,artist,song = link + try: + log('%s: search url: %s' % (__title__, url), debug=self.DEBUG) + response = requests.get(url, headers=headers, timeout=10) + result = response.text + except: + return None + soup = BeautifulSoup(result, 'html.parser') + lyr = soup.find_all('span', {'class': 'lyrics__content__ok'}) + if lyr: + lyrics = '' + for part in lyr: + lyrics = lyrics + part.get_text() + '\n' + return lyrics + else: + lyr = soup.find_all('span', {'class': 'lyrics__content__error'}) + if lyr: + lyrics = '' + for part in lyr: + lyrics = lyrics + part.get_text() + '\n' + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/lyricsScraper.py new file mode 100644 index 00000000000..783f818e8ce --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/lyricsScraper.py @@ -0,0 +1,117 @@ +#-*- coding: UTF-8 -*- +''' +Scraper for https://www.musixmatch.com/ + +musixmatchlrc + +https://github.com/rtcq/syncedlyrics +''' + +import requests +import json +import time +import difflib +import xbmcvfs +from lib.utils import * + +__title__ = "musixmatchlrc" +__priority__ = '100' +__lrc__ = True + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + self.SEARCH_URL = 'https://apic-desktop.musixmatch.com/ws/1.1/%s' + self.session = requests.Session() + self.session.headers.update( + { + "authority": "apic-desktop.musixmatch.com", + "cookie": "AWSELBCORS=0; AWSELB=0", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0", + } + ) + self.current_time = int(time.time()) + + def get_token(self): + self.token = '' + tokenpath = os.path.join(PROFILE, 'musixmatch_token') + if xbmcvfs.exists(tokenpath): + tokenfile = xbmcvfs.File(tokenpath) + tokendata = json.load(tokenfile) + tokenfile.close() + cached_token = tokendata.get("token") + expiration_time = tokendata.get("expiration_time") + if cached_token and expiration_time and self.current_time < expiration_time: + self.token = cached_token + if not self.token: + try: + url = self.SEARCH_URL % 'token.get' + query = [('user_language', 'en'), ('app_id', 'web-desktop-app-v1.0'), ('t', self.current_time)] + response = self.session.get(url, params=query, timeout=10) + result = response.json() + except: + return None + if 'message' in result and 'body' in result["message"] and 'user_token' in result["message"]["body"]: + self.token = result["message"]["body"]["user_token"] + expiration_time = self.current_time + 600 + tokendata = {} + tokendata['token'] = self.token + tokendata['expiration_time'] = expiration_time + tokenfile = xbmcvfs.File(tokenpath, 'w') + json.dump(tokendata, tokenfile) + tokenfile.close() + return self.token + + def get_lyrics(self, song): + self.token = self.get_token() + if not self.token: + return + log("%s: searching lyrics for %s - %s" % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = song.artist.replace(' ', '+') + title = song.title.replace(' ', '+') + search = '%s - %s' % (artist, title) + try: + url = self.SEARCH_URL % 'track.search' + query = [('q', search), ('page_size', '5'), ('page', '1'), ('s_track_rating', 'desc'), ('quorum_factor', '1.0'), ('app_id', 'web-desktop-app-v1.0'), ('usertoken', self.token), ('t', self.current_time)] + response = requests.get(url, params=query, timeout=10) + result = response.json() + except: + return None + links = [] + if 'message' in result and 'body' in result["message"] and 'track_list' in result["message"]["body"] and result["message"]["body"]["track_list"]: + for item in result["message"]["body"]["track_list"]: + artistname = item['track']['artist_name'] + songtitle = item['track']['track_name'] + trackid = item['track']['track_id'] + if (difflib.SequenceMatcher(None, artist.lower(), artistname.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, title.lower(), songtitle.lower()).ratio() > 0.8): + links.append((artistname + ' - ' + songtitle, trackid, artistname, songtitle)) + if len(links) == 0: + return None + elif len(links) > 1: + lyrics.list = links + for link in links: + lyr = self.get_lyrics_from_list(link) + if lyr: + lyrics.lyrics = lyr + return lyrics + return None + + def get_lyrics_from_list(self, link): + title,trackid,artist,song = link + try: + log('%s: search track id: %s' % (__title__, trackid), debug=self.DEBUG) + url = self.SEARCH_URL % 'track.subtitle.get' + query = [('track_id', trackid), ('subtitle_format', 'lrc'), ('app_id', 'web-desktop-app-v1.0'), ('usertoken', self.token), ('t', self.current_time)] + response = requests.get(url, params=query, timeout=10) + result = response.json() + except: + return None + if 'message' in result and 'body' in result["message"] and 'subtitle' in result["message"]["body"] and 'subtitle_body' in result["message"]["body"]["subtitle"]: + lyrics = result["message"]["body"]["subtitle"]["subtitle_body"] + return lyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/__init__.py new file mode 100644 index 00000000000..b93054b3ecf --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/lyricsScraper.py new file mode 100644 index 00000000000..e6f556d63c6 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/supermusic/lyricsScraper.py @@ -0,0 +1,64 @@ +#-*- coding: UTF-8 -*- +import sys +import re +import requests +import html +import xbmc +import xbmcaddon +from lib.utils import * + +__title__ = 'supermusic' +__priority__ = '250' +__lrc__ = False + + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + + def get_lyrics(self, song): + log('%s: searching lyrics for %s - %s' % (__title__, song.artist, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + artist = song.artist.lower() + title = song.title.lower() + + try: + req = requests.post('https://supermusic.cz/najdi.php', data={'hladane': title, 'typhladania': 'piesen', 'fraza': 'off'}) + response = req.text + except: + return None + req.close() + url = None + try: + items = re.search(r'Počet nájdených piesní.+

(.*)
', response, re.S).group(1) + for match in re.finditer(r'
"[^"]+?") target="_parent">(?P.*?) - (?P.+?) \((.*?)', response, re.S).group(1) + lyr = re.sub(r'.*?', '', lyr) + lyr = re.sub(r'\s*', '\n', lyr) + lyr = re.sub(r'', '', lyr, flags=re.DOTALL) + lyr = re.sub(r'<[^>]*?>', '', lyr, flags=re.DOTALL) + lyr = lyr.strip('\r\n') + lyr = html.unescape(lyr) + lyrics.lyrics = lyr + return lyrics + except: + return None diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/embedlrc.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/embedlrc.py new file mode 100644 index 00000000000..c66d410aca9 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/embedlrc.py @@ -0,0 +1,183 @@ +from mutagen.flac import FLAC +from mutagen.mp3 import MP3 +from mutagen.mp4 import MP4 +from mutagen.oggvorbis import OggVorbis +from mutagen.apev2 import APEv2 +from lib.utils import * + +LANGUAGE = ADDON.getLocalizedString + + +class BinaryFile(xbmcvfs.File): + def read(self, numBytes): + if numBytes == 0: + return b"" + else: + return bytes(super().readBytes(numBytes)) + + +def getEmbedLyrics(song, getlrc, lyricssettings): + lyrics = Lyrics(settings=lyricssettings) + lyrics.song = song + lyrics.source = LANGUAGE(32002) + lyrics.lrc = getlrc + lry = lyrics.song.embed + if lry: + match = isLRC(lry) + if (getlrc and match) or ((not getlrc) and (not match)): + if lyrics.song.source: + lyrics.source = lyrics.song.source + lyrics.lyrics = lry + return lyrics + filename = song.filepath + ext = os.path.splitext(filename)[1].lower() + sup_ext = ['.mp3', '.flac', '.ogg', '.ape', '.m4a'] + lry = None + if ext in sup_ext: + bfile = BinaryFile(filename) + if ext == '.mp3': + lry = getID3Lyrics(bfile, getlrc) + if not lry: + try: + lry = getLyrics3(bfile, getlrc) + except: + pass + elif ext == '.flac': + lry = getFlacLyrics(bfile, getlrc) + elif ext == '.m4a': + lry = getMP4Lyrics(bfile, getlrc) + elif ext == '.ogg': + lry = getOGGLyrics(bfile, getlrc) + elif ext == '.ape': + lry = getAPELyrics(bfile, getlrc) + bfile.close() + if not lry: + return None + lyrics.lyrics = lry + return lyrics + +''' +Get lyrics embed with Lyrics3/Lyrics3V2 format +See: http://id3.org/Lyrics3 + http://id3.org/Lyrics3v2 +''' +def getLyrics3(bfile, getlrc): + bfile.seek(-128-9, os.SEEK_END) + buf = bfile.read(9) + if (buf != b'LYRICS200' and buf != b'LYRICSEND'): + bfile.seek(-9, os.SEEK_END) + buf = bfile.read(9) + if (buf == b'LYRICSEND'): + ''' Find Lyrics3v1 ''' + bfile.seek(-5100-9-11, os.SEEK_CUR) + buf = bfile.read(5100+11) + start = buf.find(b'LYRICSBEGIN') + data = buf[start+11:] + enc = chardet.detect(data) + content = data.decode(enc['encoding']) + if (getlrc and isLRC(content)) or (not getlrc and not isLRC(content)): + return content + elif (buf == b'LYRICS200'): + ''' Find Lyrics3v2 ''' + bfile.seek(-9-6, os.SEEK_CUR) + size = int(bfile.read(6)) + bfile.seek(-size-6, os.SEEK_CUR) + buf = bfile.read(11) + if(buf == b'LYRICSBEGIN'): + buf = bfile.read(size-11) + tags=[] + while buf!= '': + tag = buf[:3] + length = int(buf[3:8]) + data = buf[8:8+length] + enc = chardet.detect(data) + content = data.decode(enc['encoding']) + if (tag == b'LYR'): + if (getlrc and isLRC(content)) or (not getlrc and not isLRC(content)): + return content + buf = buf[8+length:] + +def ms2timestamp(ms): + mins = '0%s' % int(ms/1000/60) + sec = '0%s' % int((ms/1000)%60) + msec = '0%s' % int((ms%1000)/10) + timestamp = '[%s:%s.%s]' % (mins[-2:],sec[-2:],msec[-2:]) + return timestamp + +''' +Get USLT/SYLT/TXXX lyrics embed with ID3v2 format +See: http://id3.org/id3v2.3.0 +''' +def getID3Lyrics(bfile, getlrc): + try: + data = MP3(bfile) + lyr = '' + for tag,value in data.items(): + if getlrc and tag.startswith('SYLT'): + for line in data[tag].text: + txt = line[0].strip() + stamp = ms2timestamp(line[1]) + lyr += '%s%s\r\n' % (stamp, txt) + elif not getlrc and tag.startswith('USLT'): + if data[tag].text: + lyr = data[tag].text + elif tag.startswith('TXXX'): + if getlrc and tag.upper().endswith('SYNCEDLYRICS'): # TXXX tags contain arbitrary info. only accept 'TXXX:SYNCEDLYRICS' + lyr = data[tag].text[0] + elif not getlrc and tag.upper().endswith('LYRICS'): # TXXX tags contain arbitrary info. only accept 'TXXX:LYRICS' + lyr = data[tag].text[0] + if lyr: + return lyr + except: + return + +def getFlacLyrics(bfile, getlrc): + try: + tags = FLAC(bfile) + if 'lyrics' in tags: + lyr = tags['lyrics'][0] + match = isLRC(lyr) + if (getlrc and match) or ((not getlrc) and (not match)): + return lyr + except: + return + +def getMP4Lyrics(bfile, getlrc): + try: + tags = MP4(bfile) + if '©lyr' in tags: + lyr = tags['©lyr'][0] + match = isLRC(lyr) + if (getlrc and match) or ((not getlrc) and (not match)): + return lyr + except: + return + +def getOGGLyrics(bfile, getlrc): + try: + tags = OggVorbis(bfile) + if 'lyrics' in tags: + lyr = tags['lyrics'][0] + match = isLRC(lyr) + if (getlrc and match) or ((not getlrc) and (not match)): + return lyr + except: + return + +def getAPELyrics(bfile, getlrc): + try: + tags = APEv2(bfile) + if 'lyrics' in tags: + lyr = tags['lyrics'][0] + match = isLRC(lyr) + if (getlrc and match) or ((not getlrc) and (not match)): + return lyr + except: + return + +def isLRC(lyr): + match = re.compile('\[(\d+):(\d\d)(\.\d+|)\]').search(lyr) + if match: + return True + else: + return False diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py new file mode 100644 index 00000000000..9b64c98414f --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py @@ -0,0 +1,268 @@ +#-*- coding: UTF-8 -*- +import time +from lib.utils import * +from lib.culrcscrapers.azlyrics import lyricsScraper as lyricsScraper_azlyrics +from lib.culrcscrapers.darklyrics import lyricsScraper as lyricsScraper_darklyrics +from lib.culrcscrapers.genius import lyricsScraper as lyricsScraper_genius +from lib.culrcscrapers.lrclib import lyricsScraper as lyricsScraper_lrclib +from lib.culrcscrapers.lyricscom import lyricsScraper as lyricsScraper_lyricscom +from lib.culrcscrapers.lyricsify import lyricsScraper as lyricsScraper_lyricsify +from lib.culrcscrapers.lyricsmode import lyricsScraper as lyricsScraper_lyricsmode +from lib.culrcscrapers.megalobiz import lyricsScraper as lyricsScraper_megalobiz +from lib.culrcscrapers.music163 import lyricsScraper as lyricsScraper_music163 +from lib.culrcscrapers.musixmatch import lyricsScraper as lyricsScraper_musixmatch +from lib.culrcscrapers.musixmatchlrc import lyricsScraper as lyricsScraper_musixmatchlrc +from lib.culrcscrapers.supermusic import lyricsScraper as lyricsScraper_supermusic + +FAILED = [] + +def test_scrapers(): + lyricssettings = {} + lyricssettings['debug'] = ADDON.getSettingBool('log_enabled') + lyricssettings['save_filename_format'] = ADDON.getSettingInt('save_filename_format') + lyricssettings['save_lyrics_path'] = ADDON.getSettingString('save_lyrics_path') + lyricssettings['save_subfolder'] = ADDON.getSettingBool('save_subfolder') + lyricssettings['save_subfolder_path'] = ADDON.getSettingString('save_subfolder_path') + + dialog = xbmcgui.DialogProgress() + TIMINGS = [] + + # test alsong + dialog.create(ADDONNAME, LANGUAGE(32163) % 'azlyrics') + log('==================== azlyrics ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'La Dispute' + song.title = 'Such Small Hands' + st = time.time() + lyrics = lyricsScraper_azlyrics.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['azlyrics',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('azlyrics') + log('FAILED: azlyrics', debug=True) + if dialog.iscanceled(): + return + + # test darklyrics + dialog.update(8, LANGUAGE(32163) % 'darklyrics') + log('==================== darklyrics ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Neurosis' + song.title = 'Lost' + st = time.time() + lyrics = lyricsScraper_darklyrics.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['darklyrics',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('darklyrics') + log('FAILED: darklyrics', debug=True) + if dialog.iscanceled(): + return + + # test genius + dialog.update(16, LANGUAGE(32163) % 'genius') + log('==================== genius ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Maren Morris' + song.title = 'My Church' + st = time.time() + lyrics = lyricsScraper_genius.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['genius',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('genius') + log('FAILED: genius', debug=True) + if dialog.iscanceled(): + return + + # test lrclib + dialog.update(24, LANGUAGE(32163) % 'lrclib') + log('==================== lrclib ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'CHVRCHES' + song.title = 'Clearest Blue' + st = time.time() + lyrics = lyricsScraper_lrclib.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['lrclib',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('lrclib') + log('FAILED: lrclib', debug=True) + if dialog.iscanceled(): + return + + # test lyricscom + dialog.update(32, LANGUAGE(32163) % 'lyricscom') + log('==================== lyricscom ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Blur' + song.title = 'You\'re So Great' + st = time.time() + lyrics = lyricsScraper_lyricscom.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['lyricscom',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('lyricscom') + log('FAILED: lyricscom', debug=True) + if dialog.iscanceled(): + return + + # test lyricsify + dialog.update(40, LANGUAGE(32163) % 'lyricsify') + log('==================== lyricsify ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Madonna' + song.title = 'Crazy For You' + st = time.time() + lyrics = lyricsScraper_lyricsify.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['lyricsify',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('lyricsify') + log('FAILED: lyricsify', debug=True) + if dialog.iscanceled(): + return + + # test lyricsmode + dialog.update(48, LANGUAGE(32163) % 'lyricsmode') + log('==================== lyricsmode ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Maren Morris' + song.title = 'My Church' + st = time.time() + lyrics = lyricsScraper_lyricsmode.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['lyricsmode',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('lyricsmode') + log('FAILED: lyricsmode', debug=True) + if dialog.iscanceled(): + return + + # test megalobiz + dialog.update(56, LANGUAGE(32163) % 'megalobiz') + log('==================== megalobiz ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Michael Jackson' + song.title = 'Beat It' + st = time.time() + lyrics = lyricsScraper_megalobiz.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['megalobiz',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('megalobiz') + log('FAILED: megalobiz', debug=True) + if dialog.iscanceled(): + return + + # test music163 + dialog.update(64, LANGUAGE(32163) % 'music163') + log('==================== music163 ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Madonna' + song.title = 'Vogue' + st = time.time() + lyrics = lyricsScraper_music163.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['music163',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('music163') + log('FAILED: music163', debug=True) + if dialog.iscanceled(): + return + + # test musixmatch + dialog.update(72, LANGUAGE(32163) % 'musixmatch') + log('==================== musixmatch ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Kate Bush' + song.title = 'Wuthering Heights' + st = time.time() + lyrics = lyricsScraper_musixmatch.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['musixmatch',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('musixmatch') + log('FAILED: musixmatch', debug=True) + if dialog.iscanceled(): + return + + # test musixmatchlrc + dialog.update(80, LANGUAGE(32163) % 'musixmatchlrc') + log('==================== musixmatchlrc ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Kate Bush' + song.title = 'Wuthering Heights' + st = time.time() + lyrics = lyricsScraper_musixmatchlrc.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['musixmatchlrc',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('musixmatchlrc') + log('FAILED: musixmatchlrc', debug=True) + if dialog.iscanceled(): + return + + # test supermusic + dialog.update(88, LANGUAGE(32163) % 'supermusic') + log('==================== supermusic ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Karel Gott' + song.title = 'Trezor' + st = time.time() + lyrics = lyricsScraper_supermusic.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['supermusic',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('supermusic') + log('FAILED: supermusic', debug=True) + if dialog.iscanceled(): + return + + dialog.close() + log('=======================================', debug=True) + log('FAILED: %s' % str(FAILED), debug=True) + log('=======================================', debug=True) + for item in TIMINGS: + log('%s - %i' % (item[0], item[1]), debug=True) + log('=======================================', debug=True) + if FAILED: + dialog = xbmcgui.Dialog().ok(ADDONNAME, LANGUAGE(32165) % ' / '.join(FAILED)) + else: + dialog = xbmcgui.Dialog().ok(ADDONNAME, LANGUAGE(32164)) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py new file mode 100644 index 00000000000..03f30cc3d48 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py @@ -0,0 +1,186 @@ +import chardet +import os +import re +import sys +import unicodedata +import xbmc +import xbmcaddon +import xbmcgui +import xbmcvfs + +ADDON = xbmcaddon.Addon() +ADDONNAME = ADDON.getAddonInfo('name') +ADDONICON = ADDON.getAddonInfo('icon') +ADDONVERSION = ADDON.getAddonInfo('version') +ADDONID = ADDON.getAddonInfo('id') +CWD = xbmcvfs.translatePath(ADDON.getAddonInfo('path')) +PROFILE = xbmcvfs.translatePath(ADDON.getAddonInfo('profile')) +LANGUAGE = ADDON.getLocalizedString + +CANCEL_DIALOG = (9, 10, 92, 216, 247, 257, 275, 61467, 61448,) +ACTION_OSD = (107, 163,) +ACTION_CODEC = (0, 27,) +ACTION_UPDOWN = (3, 4, 105, 106, 111, 112, 603, 604) +LYRIC_SCRAPER_DIR = os.path.join(CWD, 'lib', 'culrcscrapers') +WIN = xbmcgui.Window(10000) + +def log(*args, **kwargs): + if kwargs['debug']: + message = '%s: %s' % (ADDONID, args[0]) + xbmc.log(msg=message, level=xbmc.LOGDEBUG) + +def deAccent(str): + return unicodedata.normalize('NFKD', str).replace('"', '') + +def get_textfile(filepath): + try: + f = xbmcvfs.File(filepath) + data = f.readBytes() + f.close() + # Detect text encoding + enc = chardet.detect(data) + if enc['encoding']: + return data.decode(enc['encoding']) + else: + return data + except: + return None + +def get_artist_from_filename(*args, **kwargs): + filename = kwargs['filename'] + SETTING_READ_FILENAME_FORMAT = kwargs['opt']['read_filename_format'] + DEBUG = kwargs['opt']['debug'] + try: + artist = '' + title = '' + basename = os.path.basename(filename) + # Artist - title.ext + if SETTING_READ_FILENAME_FORMAT == 0: + artist = basename.split('-', 1)[0].strip() + title = os.path.splitext(basename.split('-', 1)[1].strip())[0] + # Artist/Album/title.ext or Artist/Album/Track (-) title.ext + elif SETTING_READ_FILENAME_FORMAT in (1,2): + artist = os.path.basename(os.path.split(os.path.split(filename)[0])[0]) + # Artist/Album/title.ext + if SETTING_READ_FILENAME_FORMAT == 1: + title = os.path.splitext(basename)[0] + # Artist/Album/Track (-) title.ext + elif SETTING_READ_FILENAME_FORMAT == 2: + title = os.path.splitext(basename)[0].split(' ', 1)[1].lstrip('-').strip() + # Track Artist - title.ext + elif SETTING_READ_FILENAME_FORMAT == 3: + at = basename.split(' ', 1)[1].strip() + artist = at.split('-', 1)[0].strip() + title = os.path.splitext(at.split('-', 1)[1].strip())[0] + # Track - Artist - title.ext + elif SETTING_READ_FILENAME_FORMAT == 4: + artist = basename.split('-', 2)[1].strip() + title = os.path.splitext(basename.split('-', 2)[2].strip())[0] + except: + # invalid format selected + log('failed to get artist and title from filename', debug=DEBUG) + return artist, title + +class Lyrics: + def __init__(self, *args, **kwargs): + settings = kwargs['settings'] + self.song = Song(opt=settings) + self.lyrics = '' + self.source = '' + self.list = None + self.lrc = False + +class Song: + def __init__(self, *args, **kwargs): + self.artist = '' + self.title = '' + self.filepath = '' + self.embed = '' + self.source = '' + self.analyze_safe = True + self.SETTING_SAVE_FILENAME_FORMAT = kwargs['opt']['save_filename_format'] + self.SETTING_SAVE_LYRICS_PATH = kwargs['opt']['save_lyrics_path'] + self.SETTING_SAVE_SUBFOLDER = kwargs['opt']['save_subfolder'] + self.SETTING_SAVE_SUBFOLDER_PATH = kwargs['opt']['save_subfolder_path'] + + def __str__(self): + return 'Artist: %s, Title: %s' % (self.artist, self.title) + + def __eq__(self, song): + return (deAccent(self.artist) == deAccent(song.artist)) and (deAccent(self.title) == deAccent(song.title)) + + def path1(self, lrc): + if lrc: + ext = '.lrc' + else: + ext = '.txt' + # remove invalid filename characters + artist = "".join(i for i in self.artist if i not in "\/:*?<>|") + title = "".join(i for i in self.title if i not in "\/:*?<>|") + if self.SETTING_SAVE_FILENAME_FORMAT == 0: + return os.path.join(self.SETTING_SAVE_LYRICS_PATH, artist, title + ext) + else: + return os.path.join(self.SETTING_SAVE_LYRICS_PATH, artist + ' - ' + title + ext) + + def path2(self, lrc): + if lrc: + ext = '.lrc' + else: + ext = '.txt' + dirname = os.path.dirname(self.filepath) + basename = os.path.basename(self.filepath) + filename = basename.rsplit('.', 1)[0] + if self.SETTING_SAVE_SUBFOLDER: + return os.path.join(dirname, self.SETTING_SAVE_SUBFOLDER_PATH, filename + ext) + else: + return os.path.join(dirname, filename + ext) + + @staticmethod + def current(*args, **kwargs): + kwargs = kwargs['opt'] + song = Song.by_offset(offset=0, opt=kwargs) + return song + + @staticmethod + def next(*args, **kwargs): + kwargs = kwargs['opt'] + song = Song.by_offset(offset=1, opt=kwargs) + if song.artist != '' and song.title != '': + return song + + @staticmethod + def by_offset(*args, **kwargs): + offset = kwargs['offset'] + SETTING_READ_FILENAME = kwargs['opt']['read_filename'] + SETTING_CLEAN_TITLE = kwargs['opt']['clean_title'] + song = Song(opt=kwargs['opt']) + if offset > 0: + offset_str = '.offset(%i)' % offset + else: + offset_str = '' + song.filepath = xbmc.getInfoLabel('Player%s.Filenameandpath' % offset_str) + song.title = xbmc.getInfoLabel('MusicPlayer%s.Title' % offset_str).replace('\\', ' & ').replace('/', ' & ').replace(' ',' ').replace(':','-').strip('.') + song.artist = xbmc.getInfoLabel('MusicPlayer%s.Artist' % offset_str).replace('\\', ' & ').replace('/', ' & ').replace(' ',' ').replace(':','-').strip('.') + song.embed = xbmc.getInfoLabel('MusicPlayer%s.Lyrics' % offset_str) + song.source = xbmc.getInfoLabel('MusicPlayer%s.Property(culrc.source)' % offset_str) + # some third party addons may insert the tracknumber in the song title + regex = re.compile('\d\d\.\s') + match = regex.match(song.title) + if match: + song.title = song.title[4:] + if xbmc.getCondVisibility('Player.IsInternetStream') or xbmc.getCondVisibility('Pvr.IsPlayingRadio'): + # disable search for embedded lyrics for internet streams + song.analyze_safe = False + if not song.artist: + # We probably listen to online radio which usually sets the song title as 'Artist - Title' (via ICY StreamTitle) + sep = song.title.find('-') + if sep > 1: + song.artist = song.title[:sep - 1].strip() + song.title = song.title[sep + 1:].strip() + # The title can contains some additional info in brackets at the end, so we remove it + song.title = re.sub(r'\([^\)]*\)$', '', song.title) + if (song.filepath and ((not song.title) or (not song.artist) or (SETTING_READ_FILENAME))): + song.artist, song.title = get_artist_from_filename(filename=song.filepath, opt=kwargs['opt']) + if SETTING_CLEAN_TITLE: + song.title = re.sub(r'\([^\)]*\)$', '', song.title) + return song