From f94e1032571b544c16e3f3c41f08998b402e5a6d Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Wed, 24 Jan 2024 23:23:14 -0600 Subject: [PATCH 1/7] 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 From 0aed545c6eb523e83ed74f3639b416a042c1b28e Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Wed, 24 Jan 2024 23:55:46 -0600 Subject: [PATCH 2/7] metadata/Music: adjust our lyrics *.py to use CU LRC unmodified by moving our CLI from *.py to common/culrcwrap.py which uses tiny Kodistubs to let the few xbmc calls work. See README for simple upgrade procedure for any future CU LRC upgrades. --- .../metadata/Music/lyrics/Kodistubs/README | 8 + .../metadata/Music/lyrics/Kodistubs/xbmc.py | 10 + .../Music/lyrics/Kodistubs/xbmcaddon.py | 29 ++ .../Music/lyrics/Kodistubs/xbmcgui.py | 38 +++ .../Music/lyrics/Kodistubs/xbmcvfs.py | 56 ++++ .../scripts/metadata/Music/lyrics/README | 93 ++++-- .../scripts/metadata/Music/lyrics/azlyrics.py | 173 +--------- .../metadata/Music/lyrics/common/__init__.py | 5 +- .../metadata/Music/lyrics/common/audiofile.py | 127 ------- .../metadata/Music/lyrics/common/broken.py | 12 + .../metadata/Music/lyrics/common/culrcwrap.py | 177 ++++++++++ .../Music/lyrics/common/filelyrics.py | 43 +++ .../metadata/Music/lyrics/common/testall.py | 15 + .../metadata/Music/lyrics/common/utilities.py | 44 --- .../metadata/Music/lyrics/darklyrics.py | 253 +------------- .../scripts/metadata/Music/lyrics/embedlrc.py | 311 +++--------------- .../metadata/Music/lyrics/filelyrics.py | 180 ++-------- .../scripts/metadata/Music/lyrics/genius.py | 197 +---------- .../scripts/metadata/Music/lyrics/lrclib.py | 195 +---------- .../metadata/Music/lyrics/lyricscom.py | 202 +----------- .../metadata/Music/lyrics/lyricsify.py | 206 +----------- .../metadata/Music/lyrics/lyricsmode.py | 188 +---------- .../metadata/Music/lyrics/megalobiz.py | 195 +---------- .../scripts/metadata/Music/lyrics/music163.py | 204 ++---------- .../metadata/Music/lyrics/musixmatch.py | 215 +----------- .../metadata/Music/lyrics/musixmatchlrc.py | 246 ++------------ .../metadata/Music/lyrics/supermusic.py | 195 +---------- .../metadata/Music/lyrics/testlyrics.pl | 78 +++++ 28 files changed, 807 insertions(+), 2888 deletions(-) create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/README create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmc.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcaddon.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcgui.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcvfs.py delete mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/common/audiofile.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/common/broken.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/common/filelyrics.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/common/testall.py delete mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/common/utilities.py create mode 100755 mythtv/programs/scripts/metadata/Music/lyrics/testlyrics.pl diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/README b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/README new file mode 100644 index 00000000000..0bb2fc70e80 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/README @@ -0,0 +1,8 @@ +This is a tiny portion of Kodistubs to translate CU LRC to MythTV. + +If CU LRC ever refers to other functions, simply copy them in here +from original Kodistubs and update as needed. + + -twitham@sbcglobal.net, 2024/01 for v34 + +source: https://github.com/romanvm/Kodistubs diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmc.py b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmc.py new file mode 100644 index 00000000000..c4ea87c4b29 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmc.py @@ -0,0 +1,10 @@ +# Tiny portion of Kodistubs to translate CU LRC to MythTV. If CU LRC +# ever refers to other functions, simply copy them in here from +# original Kodistubs and update where needed like below. + +import sys + +LOGDEBUG = 0 + +def log(msg: str, level: int = LOGDEBUG) -> None: + print(msg, file=sys.stderr) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcaddon.py b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcaddon.py new file mode 100644 index 00000000000..a0d2149e5f6 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcaddon.py @@ -0,0 +1,29 @@ +# Tiny portion of Kodistubs to translate CU LRC to MythTV. If CU LRC +# ever refers to other functions, simply copy them in here from +# original Kodistubs and update where needed like below. + +from typing import Optional + +class Addon: + + def __init__(self, id: Optional[str] = None) -> None: + pass + + def getLocalizedString(self, id: int) -> str: + # only testall.py / scrapertest.py needs only 1 message from + # resources/language/resource.language.en_us/strings.po + if (id == 32163): + return "Testing: %s" + return "(%s)" % id + + def getSettingBool(self, id: str) -> bool: + return True + + def getSettingInt(self, id: str) -> int: + return 0 + + def getSettingString(self, id: str) -> str: + return "" + + def getAddonInfo(self, id: str) -> str: + return "" diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcgui.py b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcgui.py new file mode 100644 index 00000000000..dee8e481166 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcgui.py @@ -0,0 +1,38 @@ +# Tiny portion of Kodistubs to translate CU LRC to MythTV. If CU LRC +# ever refers to other functions, simply copy them in here from +# original Kodistubs and update where needed like below. + +# show "dialog" on stderr for testall.py / scrapertest.py +import sys + +class Dialog: + + def __init__(self) -> None: + pass + + def ok(self, heading: str, message: str) -> bool: + return True + +class DialogProgress: + + def __init__(self) -> None: + pass + + def create(self, heading: str, message: str = "") -> None: + print("\tDIALOG created: ", heading, " : ", message, file=sys.stderr) + pass + + def update(self, percent: int, message: str = "") -> None: + print("\tDIALOG updated %s: " % percent, message, file=sys.stderr) + pass + + def close(self) -> None: + pass + + def iscanceled(self) -> bool: + # not cancelled is needed to continue the testall.py / scrapertest.py + return False + +class Window: + def __init__(self, existingWindowId: int = -1) -> None: + pass diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcvfs.py b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcvfs.py new file mode 100644 index 00000000000..e31ae388732 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/Kodistubs/xbmcvfs.py @@ -0,0 +1,56 @@ +# Tiny portion of Kodistubs to translate CU LRC to MythTV. If CU LRC +# ever refers to other functions, simply copy them in here from +# original Kodistubs and update where needed like below. + +from typing import Union, Optional +import os.path + +# embedlrc.py and musixmatchlrc.py need some File access + +class File: + + def __init__(self, filepath: str, mode: Optional[str] = None) -> None: + self.filename = filepath + if mode: + self.fh = open(filepath, mode) + else: + self.fh = open(filepath, "rb") + + def __enter__(self) -> 'File': # Required for context manager + return self + + def __exit__(self, exc_type, exc_val, exc_tb): # Required for context manager + pass + + def read(self, numBytes: int = 0) -> str: + if numBytes: + return self.fh.read(numBytes) + return self.fh.read() + + def readBytes(self, numBytes: int = 0) -> bytearray: + if numBytes: + return bytearray(self.fh.read(numBytes)) + return bytearray(self.fh.read()) + + def write(self, buffer: Union[str, bytes, bytearray]) -> bool: + return self.fh.write(buffer) + + def size(self) -> int: + return 0 + + def seek(self, seekBytes: int, iWhence: int = 0) -> int: + return self.fh.seek(seekBytes, iWhence); + + def tell(self) -> int: + return self.fh.tell() + + def close(self) -> None: + self.fh.close() + pass + +def exists(path: str) -> bool: + # for musixmatchlrc.py the test must work or return False + return os.path.isfile(path) + +def translatePath(path: str) -> str: + return "" diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/README b/mythtv/programs/scripts/metadata/Music/lyrics/README index e2aebc19351..169fdea34e5 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/README +++ b/mythtv/programs/scripts/metadata/Music/lyrics/README @@ -1,10 +1,80 @@ -Lyrics Grabbers -=============== +Lyrics Grabbers for MythMusic of MythTV +======================================= You can add your own grabbers to this directory and MythMusic will automatically use them so long as they comply with the other grabbers. -The grabber should support these command line options :- +The included grabbers are sourced from the CU LRC Kodi addon: + + https://gitlab.com/ronie/script.cu.lrclyrics + + +Copyright / LICENSE +=================== + +CU LRC and therefore this directory are licensed under the terms of +the GNU GPL version 2, see ./LICENSE.txt for details. + + +Grabbers as/of 2024/01, Their Priority And Whether Synchronized +=============================================================== + +EmbeddedLyrics 50 Yes/No +FileLyrics 90 Yes/No +musixmatchlrc 100 Yes NEW in v34 +lrclib 110 Yes NEW in v34 +lyricsify 130 Yes NEW in v34 +genius 200 No +musixmatch 210 No NEW in v34 +lyricsmode 220 No +azlyrics 230 No NEW in v34 +lyricscom 240 No +supermusic 250 No NEW in v34 +darklyrics 260 No +megalobiz 400 Yes (too slow to be earlier, was 140) +music163 500 Yes (sometimes returns author only, was 120) + + +To Upgrade to latest CU LRC +=========================== + +Simply overwrite lib/ with the latest from the source: + + https://gitlab.com/ronie/script.cu.lrclyrics + +* remove any ./*.py that has been removed from lib/culrcscrapers + +* add a new ./*.py for any new scraper in lib/culrcscrapers + +* adjust the fields of info{} as needed + +* overwrite ./changelog.txt from the source, compare its head to line + 2 of addon.xml in the source and update the latest version number + into the header of ./common/culrcwrap.py + +* ./testlyrics -k -t + +That's all! + +As of v34 in 2024/01 this is how we use CU LRC unmodified: + +Only a small boilerplate bootstrap file of a few lines of information +is needed in this directory. These files import only the +LyricsFetcher from ./lib/culrcscrapers or from ./common for any +additional grabbers like filelyrics.py. Finally they run main from +./common/culrcwrap.py which implements the below CLI translation to +MythTV via a very tiny and simple ./Kodistubs. + +Custom grabbers can similarly use ./common/culrcwrap.py for the CLI +and simply put their LyricsFetcher in ./common, like filelyrics did. + + +Required command line options for all grabbers +============================================== + +(automatically done by ./common/culrcwrap.py) + +All grabbers should support these command line options: Options: -h, --help show this help message and exit @@ -87,20 +157,3 @@ Options: -Current Grabbers, Their Priority And Whether Synchronized -========================================================= - -EmbeddedLyrics 50 Yes/No -FileLyrics 90 Yes/No -musixmatchlrc 100 Yes NEW in v34 -lrclib 110 Yes NEW in v34 -lyricsify 130 Yes NEW in v34 -genius 200 No -musixmatch 210 No NEW in v34 -lyricsmode 220 No -azlyrics 230 No NEW in v34 -lyricscom 240 No -supermusic 250 No NEW in v34 -darklyrics 260 No -megalobiz 400 Yes (too slow to be earlier, was 140) -music163 500 Yes (returns incomplete results, was 120) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/azlyrics.py b/mythtv/programs/scripts/metadata/Music/lyrics/azlyrics.py index 7ddaa8b40a1..24a9b59603f 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/azlyrics.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/azlyrics.py @@ -1,159 +1,18 @@ -#-*- coding: UTF-8 -*- -""" -Scraper for http://www.azlyrics.com/ -ronie -""" - -import sys -import re -import requests -import html -from optparse import OptionParser -from common import utilities - -__author__ = "ronie" -__title__ = "Azlyrics" -__description__ = "Search http://www.azlyrics.com/ for lyrics" -__version__ = "0.1" -__priority__ = "230" -__syncronized__ = False - -debug = False - -class LyricsFetcher: - def __init__( self ): - self.url = 'https://www.azlyrics.com/lyrics/%s/%s.html' - - def get_lyrics(self, lyrics): - utilities.log(debug, '%s: searching lyrics for %s - %s' % (__title__, lyrics.artist, lyrics.title)) - artist = re.sub("[^a-zA-Z0-9]+", "", lyrics.artist).lower().lstrip('the ') - title = re.sub("[^a-zA-Z0-9]+", "", lyrics.title).lower() - try: - req = requests.get(self.url % (artist, title), timeout=10) - response = req.text - - except: - return False - req.close() - try: - lyricscode = response.split('t. -->')[1].split('
', '\n') - lyr = re.sub('<[^<]+?>', '', lyricstext) - lyrics.lyrics = lyr - return True - except: - return False - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'azlyrics.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Perform self-test for dependencies.") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - if (len(args) > 0): - utilities.log('ERROR: invalid arguments found') - sys.exit(1) - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.azlyrics.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'AZLyrics', + 'description': 'Search https://azlyrics.com for lyrics', + 'author': 'ronie', + 'priority': '230', + 'syncronized': False, + 'artist': 'La Dispute', + 'title': 'Such Small Hands', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/__init__.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/__init__.py index 391fdc6bbe8..8c0d5d5bb20 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/common/__init__.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/common/__init__.py @@ -1,4 +1 @@ -__version__ = "1.0.0" - -from . import utilities, audiofile - +__version__ = "2.0.0" diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/audiofile.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/audiofile.py deleted file mode 100644 index 43d12c06ecc..00000000000 --- a/mythtv/programs/scripts/metadata/Music/lyrics/common/audiofile.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -""" -read audio stream from audio file -""" - -import os -import struct - -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 = 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.read(len) - - def AnalyzeMp3(self): - # Searching ID3v2 tag - while True: - buf = self.f.read(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 == 'ID3': - self.f.seek(3,1) # skip version/flag - # ID length (synchsafe integer) - tl = struct.unpack('4b', self.f.read(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.read(1) - if len(buf) < 1 or self.f.seek(0,1) > 1000000: - raise FormatError - if buf == '\xff': - rbit = struct.unpack('B',self.f.read(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.read(27) # header - if len(buf) < 27 or self.f.tell() > 50000: - # parse error - raise FormatError - if buf[0:4] != '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.read(numseg)) # segment table - for seglen in segtbl: - buf = self.f.read(7) # segment header - #print "segLen(%s): %d" % (buf[1:7],seglen) - if buf == "\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.read(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/common/broken.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/broken.py new file mode 100644 index 00000000000..54a56c794a2 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/common/broken.py @@ -0,0 +1,12 @@ +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- +""" +Scraper for broken scrapers - do nothing +""" + +class LyricsFetcher: + def __init__(self, *args, **kwargs): + self.DEBUG = kwargs['debug'] + self.settings = kwargs['settings'] + + def get_lyrics(self, song): + return False diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py new file mode 100644 index 00000000000..da62d2f29c0 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py @@ -0,0 +1,177 @@ +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- +""" +Wrapper for using CU LRC scrapers with MythMusic of MythTV + +Paul Harrison, ronie, Timothy Witham +""" + +# UPDATE THIS from https://gitlab.com/ronie/script.cu.lrclyrics +# second line of its addon.xml: +__version__ = '6.6.2' + +# simulate kodi/xbmc via very simplified Kodistubs. +import os +import sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)) + '/../Kodistubs') +from lib.utils import * +from optparse import OptionParser + +# album is never searched, but it is given in the xml to mythtv +class Song(Song): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # from ../lib/utils.py + self.album = '' + +debug = False + +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') + +def getCacheDir(): + confdir = os.environ.get('MYTHCONFDIR', '') + + if (not confdir) or (confdir == '/'): + confdir = os.environ.get('HOME', '') + + if (not confdir) or (confdir == '/'): + log("Unable to find MythTV directory for metadata cache.", debug=True) + return '/tmp' + + confdir = os.path.join(confdir, '.mythtv') + cachedir = os.path.join(confdir, 'cache') + + if not os.path.exists(cachedir): + os.makedirs(cachedir) + + return cachedir + +def performSelfTest(): + try: + from bs4 import BeautifulSoup + except: + log("Failed to import BeautifulSoup. This grabber requires python-bs4", debug=True) + sys.exit(1) + + found = False + song = Song(opt=lyricssettings) + song.artist = about.get('artist') + song.title = about.get('title') + song.album = about.get('album') + song.filepath = about.get('filename') + + fetcher = LyricsFetcher(settings=lyricssettings, debug=True) + lyrics = fetcher.get_lyrics(song) + + if lyrics: + if debug: + print(lyrics.lyrics) + try: + buildLyrics(song, lyrics) + except: + log("Failed to build lyrics xml file. " + "Maybe you don't have lxml installed?", debug=True) + sys.exit(1) + + log("Everything appears in order.", debug=True) + sys.exit(0) + + log("Failed to find the lyrics for the test search!", debug=True) + sys.exit(1) + +def buildLyrics(song, lyrics): + from lxml import etree + xml = etree.XML(u'') + etree.SubElement(xml, "artist").text = song.artist + etree.SubElement(xml, "album").text = song.album + etree.SubElement(xml, "title").text = song.title + etree.SubElement(xml, "syncronized").text = 'True' if about['syncronized'] else 'False' + etree.SubElement(xml, "grabber").text = about['name'] + + lines = lyrics.lyrics.splitlines() + for line in lines: + etree.SubElement(xml, "lyric").text = line + + print(etree.tostring(xml, encoding='UTF-8', + pretty_print=True, xml_declaration=True).decode()) + +def buildVersion(): + from lxml import etree + xml = etree.XML(u'') + etree.SubElement(xml, "name").text = about['name'] + etree.SubElement(xml, "author").text = about['author'] + etree.SubElement(xml, "command").text = about['command'] + etree.SubElement(xml, "type").text = 'lyrics' + etree.SubElement(xml, "description").text = about['description'] + etree.SubElement(xml, "version").text = about['version'] + etree.SubElement(xml, "priority").text = about['priority'] + etree.SubElement(xml, "syncronized").text = 'True' if about['syncronized'] else 'False' + + print(etree.tostring(xml, encoding='UTF-8', + pretty_print=True, xml_declaration=True).decode()) + sys.exit(0) + +def main(filename, info, fetcher): + global debug + global about + about = info + about['command'] = os.path.basename(filename) + if not about.get('version'): + about['version'] = __version__ + if not about.get('album'): + about['album'] = '' + global LyricsFetcher + LyricsFetcher = fetcher + + parser = OptionParser() + + parser.add_option('-v', "--version", action="store_true", default=False, + dest="version", help="Display version and author") + parser.add_option('-t', "--test", action="store_true", default=False, + dest="test", help="Test grabber with a known good search") + parser.add_option('-s', "--search", action="store_true", default=False, + dest="search", help="Search for lyrics.") + parser.add_option('-a', "--artist", metavar="ARTIST", default=None, + dest="artist", help="Artist of track.") + parser.add_option('-b', "--album", metavar="ALBUM", default=None, + dest="album", help="Album of track.") + parser.add_option('-n', "--title", metavar="TITLE", default=None, + dest="title", help="Title of track.") + parser.add_option('-f', "--filename", metavar="FILENAME", default=None, + dest="filename", help="Filename of track.") + parser.add_option('-d', '--debug', action="store_true", default=False, + dest="debug", help=("Show debug messages")) + + opts, args = parser.parse_args() + + song = Song(opt=lyricssettings) + + if opts.debug: + debug = True + + if opts.version: + buildVersion() + + if opts.test: + performSelfTest() + + if opts.artist: + song.artist = opts.artist + if opts.album: + song.album = opts.album + if opts.title: + song.title = opts.title + if opts.filename: + song.filepath = opts.filename + + fetcher = LyricsFetcher(settings=lyricssettings, debug=debug) + lyrics = fetcher.get_lyrics(song) + if lyrics: + buildLyrics(song, lyrics) + sys.exit(0) + else: + log("No lyrics found for this track", debug=True) + sys.exit(1) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/filelyrics.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/filelyrics.py new file mode 100644 index 00000000000..23c0d4e547a --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/common/filelyrics.py @@ -0,0 +1,43 @@ +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- +""" +Scraper for file lyrics +""" + +from lib.utils import * + +__title__ = "FileLyrics" +__description__ = "Search the same directory as the track for lyrics" +__author__ = "Paul Harrison" +__version__ = "2.0" +__priority__ = "90" +__lrc__ = True + +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 - %s" % (__title__, song.artist, song.album, song.title), debug=self.DEBUG) + lyrics = Lyrics(settings=self.settings) + lyrics.song = song + lyrics.source = __title__ + lyrics.lrc = __lrc__ + + filename = song.filepath + filename = os.path.splitext(filename)[0] + + # look for a file ending in .lrc with the same filename as the track minus the extension + lyricFile = filename + '.lrc' + log("%s: searching for lyrics file: %s " % (__title__, lyricFile), debug=self.DEBUG) + if os.path.exists(lyricFile) and os.path.isfile(lyricFile): + #load the text file + with open (lyricFile, "r") as f: + lines = f.readlines() + + for line in lines: + lyrics.lyrics += line + + return lyrics + + return False diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/testall.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/testall.py new file mode 100644 index 00000000000..833d2fc0035 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/common/testall.py @@ -0,0 +1,15 @@ +#-*- coding: utf-8 -*- + +# simulate kodi/xbmc via very simplified Kodistubs. +import os +import sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)) + '/../Kodistubs') +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)) + '/..') + +from lib.scrapertest import * + +def main(): + test_scrapers(); + +if __name__ == '__main__': + main() diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/utilities.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/utilities.py deleted file mode 100644 index bec61393340..00000000000 --- a/mythtv/programs/scripts/metadata/Music/lyrics/common/utilities.py +++ /dev/null @@ -1,44 +0,0 @@ -import sys -import os -import unicodedata - -def log(debug, txt): - if debug: - print(txt) - -class Lyrics: - def __init__(self): - self.artist = "" - self.album = "" - self.title = "" - self.filename = "" - self.lyrics = "" - self.source = "" - self.list = None - self.syncronized = False - -def deAccent(str): - return unicodedata.normalize('NFKD', str).replace('"', '') - -def convert_etree(etostr): - """lxml.etree.tostring is a bytes object in python3, and a str in python2. - """ - return(etostr.decode()) - -def getCacheDir(): - confdir = os.environ.get('MYTHCONFDIR', '') - - if (not confdir) or (confdir == '/'): - confdir = os.environ.get('HOME', '') - - if (not confdir) or (confdir == '/'): - print ("Unable to find MythTV directory for metadata cache.") - return '/tmp' - - confdir = os.path.join(confdir, '.mythtv') - cachedir = os.path.join(confdir, 'cache') - - if not os.path.exists(cachedir): - os.makedirs(cachedir) - - return cachedir diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/darklyrics.py b/mythtv/programs/scripts/metadata/Music/lyrics/darklyrics.py index 460cd557f0f..96567f765ee 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/darklyrics.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/darklyrics.py @@ -1,239 +1,18 @@ -#-*- 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 urllib.parse -import re -import time -import chardet - -try: - from ctypes import c_int32 # ctypes not supported on xbox -except: - pass - -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and smory" -__title__ = "DarkLyrics" -__description__ = "Search http://www.darklyrics.com/ - the largest metal lyrics archive on the Web" -__priority__ = "260" -__version__ = "0.3" -__syncronized__ = False - -debug = False - -class LyricsFetcher: - - def __init__( self ): - 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 name - 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, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - links = self.search(lyrics.artist, lyrics.title) - if(links == None or len(links) == 0): - return False - elif len(links) > 1: - lyrics.list = links - lyr = self.get_lyrics_from_list(links[0]) - if not lyr: - return False - lyrics.lyrics = lyr - return True - - - def get_lyrics_from_list(self, link): - title, url, artist, song, index = link - return self.findLyrics(url, index) - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Neurosis' - lyrics.album = '' - lyrics.title = 'Lost' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - line2 = re.sub(u'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\u10000-\u10FFFF]+', '', line) - etree.SubElement(xml, "lyric").text = line2 - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'darklyrics.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.darklyrics.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'DarkLyrics', + 'description': 'Search http://www.darklyrics.com/ - the largest metal lyrics archive on the Web', + 'author': 'Paul Harrison and smory', + 'priority': '260', + 'syncronized': False, + 'artist': 'Neurosis', + 'title': 'Lost', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/embedlrc.py b/mythtv/programs/scripts/metadata/Music/lyrics/embedlrc.py index ad163328fa9..fb631730157 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/embedlrc.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/embedlrc.py @@ -1,271 +1,46 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- -# ---------------------- -""" -Scraper for embedded lyrics -""" - -import sys, os, re, chardet -import xml.dom.minidom as xml -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and ronin" -__title__ = "EmbeddedLyrics" -__description__ = "Search tracks tag for embedded lyrics" -__version__ = "0.2" -__priority__ = "50" -__syncronized__ = True - -debug = False - -def getLyrics3(filename): - #Get lyrics embed with Lyrics3/Lyrics3V2 format - #See: http://id3.org/Lyrics3 - #http://id3.org/Lyrics3v2 - - utilities.log(debug, "%s: trying %s" % (__title__, "lyrics embed with Lyrics3/Lyrics3V2 format")) - - f = File(filename) - f.seek(-128-9, os.SEEK_END) - buf = f.read(9) - if (buf != "LYRICS200" and buf != "LYRICSEND"): - f.seek(-9, os.SEEK_END) - buf = f.read(9) - if (buf == "LYRICSEND"): - """ Find Lyrics3v1 """ - f.seek(-5100-9-11, os.SEEK_CUR) - buf = f.read(5100+11) - f.close(); - start = buf.find("LYRICSBEGIN") - elif (buf == "LYRICS200"): - """ Find Lyrics3v2 """ - f.seek(-9-6, os.SEEK_CUR) - size = int(f.read(6)) - f.seek(-size-6, os.SEEK_CUR) - buf = f.read(11) - if(buf == "LYRICSBEGIN"): - buf = f.read(size-11) - tags=[] - while buf!= '': - tag = buf[:3] - length = int(buf[3:8]) - content = buf[8:8+length] - if (tag == 'LYR'): - return content - buf = buf[8+length:] - f.close(); - return None - -def endOfString(string, utf16=False): - if (utf16): - pos = 0 - while True: - pos += string[pos:].find('\x00\x00') + 1 - if (pos % 2 == 1): - return pos - 1 - else: - return string.find('\x00') - -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 - -# Uses the high level interface in taglib to find the lyrics -# should work with all the tag formats supported by taglib that can have lyrics -def getLyricsGeneric(filename): - try: - import taglib - except: - utilities.log(True, "Failed to import taglib. This grabber requires " - "pytaglib TagLib bindings for Python. " - "https://github.com/supermihi/pytaglib") - return None - - try: - utilities.log(debug, "%s: trying to open %s" % (__title__, filename)) - f = taglib.File(filename) - - # see if we can find a lyrics tag - for tag in f.tags: - if tag.startswith('LYRICS'): - return f.tags[tag][0] - - return None - except: - return None - -# Get USLT/SYLT/TXXX lyrics embed with ID3v2 format -# See: http://id3.org/id3v2.3.0 -def getID3Lyrics(filename): - utilities.log(debug, "%s: trying %s" % (__title__, "lyrics embed with ID3v2 format")) - - # just use the generic taglib method for now - return getLyricsGeneric(filename) - -def getFlacLyrics(filename): - utilities.log(debug, "%s: trying %s" % (__title__, "lyrics embed with Flac format")) - - # just use the generic taglib method for now - return getLyricsGeneric(filename) - -def getMP4Lyrics(filename): - utilities.log(debug, "%s: trying %s" % (__title__, "lyrics embed with MP4 format")) - - # just use the generic taglib method for now - return getLyricsGeneric(filename) - +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +import os +from common import culrcwrap +from lib.embedlrc import * +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': '*EmbeddedLyrics', + 'description': 'Search track tags for embedded lyrics', + 'author': 'Paul Harrison and ronie', + 'priority': '50', # first, before filelyrics + 'syncronized': True, + 'artist': 'Robb Benson', + 'title': 'Lone Rock', + 'album': 'Demo Tracks', + 'filename': os.path.dirname(os.path.abspath(__file__)) + '/examples/taglyrics.mp3', +} + +# lib/embedlrc.py has no LyricsFetcher, so we create it here: class LyricsFetcher: - - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - filename = lyrics.filename - - ext = os.path.splitext(filename)[1].lower() - lry = None - - try: - if ext == '.mp3': - lry = getLyrics3(filename) - except: - pass - - if lry: - enc = chardet.detect(lry) - lyrics.lyrics = lry.decode(enc['encoding']) - else: - if ext == '.mp3': - lry = getID3Lyrics(filename) - elif ext == '.flac': - lry = getFlacLyrics(filename) - elif ext == '.m4a': - lry = getMP4Lyrics(filename) - if not lry: - return False - lyrics.lyrics = lry - - return True - - -def performSelfTest(): - try: - import taglib - except: - utilities.log(True, "Failed to import taglib. This grabber requires " - "pytaglib ? TagLib bindings for Python. " - "https://github.com/supermihi/pytaglib") - sys.exit(1) - - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Robb Benson' - lyrics.album = 'Demo Tracks' - lyrics.title = 'Lone Rock' - lyrics.filename = os.path.dirname(os.path.abspath(__file__)) + '/examples/taglyrics.mp3' - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'embedlrc.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Perform self-test for dependencies.") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) + 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" + % (info['name'], song.artist, song.title), debug=self.DEBUG) + log("%s: searching file %s" + % (info['name'], song.filepath), debug=self.DEBUG) + log("%s: searching for SYNCHRONIZED lyrics" + % info['name'], debug=self.DEBUG) + lrc = getEmbedLyrics(song, True, culrcwrap.lyricssettings) + if lrc: + return lrc + log("%s: searching for NON-synchronized lyrics" + % info['name'], debug=self.DEBUG) + lrc = getEmbedLyrics(song, False, culrcwrap.lyricssettings) + if lrc: + return lrc + return None if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) + +# most of the code moved to lib/embedlrc.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/filelyrics.py b/mythtv/programs/scripts/metadata/Music/lyrics/filelyrics.py index 04c35cc22b4..98525097cfd 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/filelyrics.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/filelyrics.py @@ -1,160 +1,24 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# ---------------------- -""" -Scraper for file lyrics -""" - -import sys, os, re, chardet -import xml.dom.minidom as xml -from optparse import OptionParser -from common import * - -__author__ = "Paul Harrison" -__title__ = "FileLyrics" -__description__ = "Search the same directory as the track for lyrics" -__version__ = "0.1" -__priority__ = "90" -__syncronized__ = True - -debug = False - - -class LyricsFetcher: - - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - filename = lyrics.filename - filename = os.path.splitext(filename)[0] - - # look for a file ending in .lrc with the same filename as the track minus the extension - lyricFile = filename + '.lrc' - utilities.log(debug, "%s: searching for lyrics file: %s " % (__title__, lyricFile)) - if os.path.exists(lyricFile) and os.path.isfile(lyricFile): - #load the text file - with open (lyricFile, "r") as f: - lines = f.readlines() - - for line in lines: - lyrics.lyrics += line - - return True - - return False; - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Robb Benson' - lyrics.album = 'Demo Tracks' - lyrics.title = 'Lone Rock' - lyrics.filename = os.path.dirname(os.path.abspath(__file__)) + '/examples/filelyrics.mp3' - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - try: - buildLyrics(lyrics) - except: - utilities.log(True, "Failed to build lyrics xml file. " - "Maybe you don't have lxml installed?") - sys.exit(1) - - utilities.log(True, "Everything appears in order.") - sys.exit(0) - - utilities.log(True, "Failed to find the lyrics for the test search!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'filelyrics.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Perform self-test for dependencies.") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +import os +from common import culrcwrap +from common.filelyrics import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': '*FileLyrics', + 'description': 'Search the same directory as the track for lyrics', + 'author': 'Paul Harrison', + 'priority': '90', # before all remote web scrapers 100+ + 'version': '2.0', + 'syncronized': True, + 'artist': 'Robb Benson', + 'title': 'Lone Rock', + 'album': 'Demo Tracks', + 'filename': os.path.dirname(os.path.abspath(__file__)) + '/examples/filelyrics.mp3', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) + +# most of the code moved to common/filelyrics.py and common/main.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/genius.py b/mythtv/programs/scripts/metadata/Music/lyrics/genius.py index 68e5daf7d89..4a43a8a63f1 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/genius.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/genius.py @@ -1,183 +1,18 @@ -#-*- coding: UTF-8 -*- -""" -Scraper for http://www.genius.com - -taxigps -""" -import sys -import re -import urllib.parse -import requests -import html -import difflib -import json - -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and ronie" -__title__ = "Genius" -__description__ = "Search http://www.genius.com for lyrics" -__priority__ = "200" -__version__ = "0.1" -__syncronized__ = False - - -debug = False - -class LyricsFetcher: - def __init__( self ): - self.url = 'http://api.genius.com/search?q=%s%%20%s&access_token=Rq_cyNZ6fUOQr4vhyES6vu1iw3e94RX85ju7S8-0jhM-gftzEvQPG7LJrrnTji11' - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - 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(lyrics.artist), urllib.parse.quote(lyrics.title)) - req = requests.get(url, headers=headers, timeout=10) - response = req.text - except: - return False - 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, lyrics.artist.lower(), name.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, lyrics.title.lower(), track.lower()).ratio() > 0.8): - self.page = data['response']['hits'][0]['result']['url'] - else: - return False - except: - return False - utilities.log(debug, '%s: search url: %s' % (__title__, self.page)) - 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 False - 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 False - lyrics.lyrics = lyr3 - return True - except: - return False - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'genius.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.genius.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'Genius', + 'description': 'Search http://www.genius.com for lyrics', + 'author': 'Paul Harrison and ronie', + 'priority': '200', + 'syncronized': False, + 'artist': 'Maren Morris', + 'title': 'My Church', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lrclib.py b/mythtv/programs/scripts/metadata/Music/lyrics/lrclib.py index b2f2db2f5d7..6a5e3b69a27 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lrclib.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lrclib.py @@ -1,181 +1,18 @@ -#-*- coding: UTF-8 -*- -''' -Scraper for https://lrclib.net/ - -lrclib - -https://github.com/rtcq/syncedlyrics -''' - -import requests -import difflib - -import sys -from optparse import OptionParser -from common import * - -__author__ = "Paul Harrison and ronie" -__title__ = "LrcLib" -__description__ = "Search https://lrclib.net for lyrics" -__priority__ = "110" -__version__ = "0.1" -__syncronized__ = True - - -debug = False - -class LyricsFetcher: - def __init__( self ): - self.SEARCH_URL = 'https://lrclib.net/api/search?q=%s-%s' - self.LYRIC_URL = 'https://lrclib.net/api/get/%i' - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - try: - url = self.SEARCH_URL % (lyrics.artist, lyrics.title) - response = requests.get(url, timeout=10) - result = response.json() - except: - return False - links = [] - for item in result: - artistname = item['artistName'] - songtitle = item['name'] - songid = item['id'] - if (difflib.SequenceMatcher(None, lyrics.artist.lower(), artistname.lower()).ratio() > 0.8) and (difflib.SequenceMatcher(None, lyrics.title.lower(), songtitle.lower()).ratio() > 0.8): - links.append((artistname + ' - ' + songtitle, self.LYRIC_URL % songid, artistname, songtitle)) - if len(links) == 0: - return False - elif len(links) > 1: - lyrics.list = links - for link in links: - lyr = self.get_lyrics_from_list(link) - if lyr: - lyrics.lyrics = lyr - return True - return False - - def get_lyrics_from_list(self, link): - title,url,artist,song = link - try: - utilities.log(debug, '%s: search url: %s' % (__title__, url)) - response = requests.get(url, timeout=10) - result = response.json() - except: - return None - if 'syncedLyrics' in result: - lyrics = result['syncedLyrics'] - return lyrics - - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'lrclib.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) - +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.lrclib.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': '*LrcLib', + 'description': 'Search https://lrclib.net for synchronized lyrics', + 'author': 'ronie', + 'priority': '110', + 'syncronized': True, + 'artist': 'CHVRCHES', + 'title': 'Clearest Blue', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lyricscom.py b/mythtv/programs/scripts/metadata/Music/lyrics/lyricscom.py index 2ed375968cb..a2a583b7101 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lyricscom.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lyricscom.py @@ -1,188 +1,18 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -""" -Scraper for http://www.lyrics.com/ - -ronie -""" - -import re -import requests -import urllib.parse -import difflib -from bs4 import BeautifulSoup - -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and ronie" -__title__ = "Lyrics.Com" -__description__ = "Search http://www.lyrics.com for lyrics" -__priority__ = "240" -__version__ = "0.1" -__syncronized__ = False - -debug = False - -class LyricsFetcher: - def __init__( self ): - self.url = 'http://www.lyrics.com/serp.php?st=%s&qtype=2' - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - sess = requests.Session() - - try: - request = sess.get(self.url % urllib.parse.quote_plus(lyrics.artist), timeout=10) - response = request.text - except: - return False - 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 False - soup = BeautifulSoup(resp, 'html.parser') - url = '' - for link in soup.find_all('a'): - if link.string and (difflib.SequenceMatcher(None, link.string.lower(), lyrics.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 False - matchcode = re.search('(.*?)', resp2, flags=re.DOTALL) - if matchcode: - lyricscode = (matchcode.group(1)) - lyr = re.sub('<[^<]+?>', '', lyricscode) - lyrics.lyrics = lyr.replace('\\n','\n') - return True - - return False - -def performSelfTest(): - try: - from bs4 import BeautifulSoup - except: - utilities.log(True, "Failed to import BeautifulSoup. This grabber requires python-bs4") - sys.exit(1) - - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'lyricscom.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.lyricscom.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'Lyrics.Com', + 'description': 'Search https://lyrics.com for lyrics', + 'author': 'Paul Harrison and ronie', + 'priority': '240', + 'syncronized': False, + 'artist': 'Blur', + 'title': "You're so Great", +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py b/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py index 89314e0e9c1..6b96885e910 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py @@ -1,192 +1,18 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -""" -Scraper for https://www.lyricsify.com/ - -ronie -""" - -import requests -import re -import difflib -from bs4 import BeautifulSoup - -import sys -from optparse import OptionParser -from common import utilities - - -__author__ = "Paul Harrison and ronie" -__title__ = "Lyricsify" -__description__ = "Search https://www.lyricsify.com for lyrics" -__priority__ = "130" -__version__ = "0.1" -__syncronized__ = True - -debug = False - -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 ): - self.SEARCH_URL = 'https://www.lyricsify.com/lyrics/%s/%s' - self.LYRIC_URL = 'https://www.lyricsify.com%s' - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - artist = lyrics.artist.replace(' ', '-') - title = lyrics.title.replace(' ', '-') - try: - url = self.SEARCH_URL % (artist, title) - search = requests.get(url, headers=UserAgent, timeout=10) - response = search.text - except: - return False - 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 False - elif len(links) > 1: - lyrics.list = links - for link in links: - lyr = self.get_lyrics_from_list(link) - if lyr: - lyrics.lyrics = lyr - return True - return False - - def get_lyrics_from_list(self, link): - title,url,artist,song = link - try: - utilities.log(debug, '%s: search url: %s' % (__title__, url)) - search = requests.get(url, headers=UserAgent, timeout=10) - response = search.text - except: - return None - matchcode = re.search('/h3>(.*?)', '', lyricscode) - return cleanlyrics - - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'lyricsify.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) - +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.lyricsify.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': '*Lyricsify', + 'description': 'Search https://lyricsify.com for synchronized lyrics', + 'author': 'ronie', + 'priority': '130', + 'syncronized': True, + 'artist': 'Madonna', + 'title': 'Crazy For You', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsmode.py b/mythtv/programs/scripts/metadata/Music/lyrics/lyricsmode.py index d0719dfa88b..472f50e8994 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsmode.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lyricsmode.py @@ -1,174 +1,18 @@ -#-*- coding: UTF-8 -*- -import requests -import urllib.parse -import re - -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and ronie" -__title__ = "LyricsMode" -__description__ = "Search http://www.lyricsmode.com for lyrics" -__priority__ = "240" -__version__ = "0.1" -__syncronized__ = False - -debug = False - -class LyricsFetcher: - def __init__( self ): - return - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - artist = utilities.deAccent(lyrics.artist) - title = utilities.deAccent(lyrics.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 True - - def direct_url(self, url): - try: - utilities.log(debug, '%s: direct url: %s' % (__title__, url)) - song_search = requests.get(url, timeout=10) - response = song_search.text - if response.find('lyrics_text') >= 0: - return response - except: - utilities.log(debug, 'error in direct url') - - def search_url(self, artist, title): - try: - url = 'http://www.lyricsmode.com/search.php?search=' + urllib.parse.quote_plus(artist.lower() + ' ' + title.lower()) - utilities.log(debug, '%s: search url: %s' % (__title__, url)) - song_search = requests.get(url, timeout=10) - response = song_search.text - matchcode = re.search('lm-list__cell-title">.*?') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'lyricsmode.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) - +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.lyricsmode.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'LyricsMode', + 'description': 'Search http://www.lyricsmode.com for lyrics', + 'author': 'Paul Harrison and ronie', + 'priority': '220', + 'syncronized': False, + 'artist': 'Maren Morris', + 'title': 'My Church', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/megalobiz.py b/mythtv/programs/scripts/metadata/Music/lyrics/megalobiz.py index 46215ab5f85..be6d3558339 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/megalobiz.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/megalobiz.py @@ -1,184 +1,21 @@ -#-*- coding: UTF-8 -*- -""" -Scraper for https://www.megalobiz.com/ +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- -megalobiz -""" +from common import culrcwrap +from lib.culrcscrapers.megalobiz.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename -import requests -import re -from bs4 import BeautifulSoup +info = { + 'name': '*Megalobiz', + 'description': 'Search https://megalobiz.com for synchronized lyrics', + 'author': 'ronie', + 'priority': '140', + 'syncronized': True, + 'artist': 'Michael Jackson', + 'title': 'Beat It', +} -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and 'ronie'" -__title__ = "Megalobiz" -__description__ = "Search https://www.megalobiz.com/ for lyrics" -__version__ = "0.1" -__priority__ = "400" -__syncronized__ = True - -debug = False - -class LyricsFetcher: - def __init__( self ): - self.SEARCH_URL = 'https://www.megalobiz.com/search/all?qry=%s-%s&searchButton.x=0&searchButton.y=0' - self.LYRIC_URL = 'https://www.megalobiz.com/%s' - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - try: - url = self.SEARCH_URL % (lyrics.artist, lyrics.title) - response = requests.get(url, timeout=10) - result = response.text - except: - return None - links = [] - soup = BeautifulSoup(result, 'html.parser') - for link in soup.find_all('a'): - if link.get('href') and link.get('href').startswith('/lrc/maker/'): - linktext = link.text.replace('_', ' ').strip() - if lyrics.artist.lower() in linktext.lower() and lyrics.title.lower() in linktext.lower(): - links.append((linktext, self.LYRIC_URL % link.get('href'), lyrics.artist, lyrics.title)) - 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 True - return False - - def get_lyrics_from_list(self, link): - title,url,artist,song = link - try: - utilities.log(debug, '%s: search url: %s' % (__title__, url)) - response = requests.get(url, timeout=10) - result = response.text - except: - return None - matchcode = re.search('span id="lrc_[0-9]+_lyrics">(.*?)', '', lyricscode) - return cleanlyrics - - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'megalobiz.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Perform self-test for dependencies.") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - if (len(args) > 0): - utilities.log('ERROR: invalid arguments found') - sys.exit(1) - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# it takes 8 seconds, have to move to the end -twitham, 2024/01 +info['priority'] = '400' if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/music163.py b/mythtv/programs/scripts/metadata/Music/lyrics/music163.py index 721474edcde..cbf56e9736b 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/music163.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/music163.py @@ -1,184 +1,24 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -""" -Scraper for http://music.163.com/ - -osdlyrics -""" - -import requests -import re -import random -import difflib - -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and ronie" -__title__ = "Music163" -__description__ = "Lyrics scraper for http://music.163.com/" -__priority__ = "500" -__version__ = "0.1" -__syncronized__ = True - -debug = 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 ): - self.SEARCH_URL = 'http://music.163.com/api/search/get' - self.LYRIC_URL = 'http://music.163.com/api/song/lyric' - - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - artist = lyrics.artist.replace(' ', '+') - title = lyrics.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 False - 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 False - 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 True - return None - - def get_lyrics_from_list(self, link): - title,url,artist,song = link - try: - utilities.log(debug, '%s: search url: %s' % (__title__, url)) - response = requests.get(url, headers=headers, timeout=10) - result = response.json() - except: - return None - if 'lrc' in result: - return result['lrc']['lyric'] - - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'music163.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.music163.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': '*Music163', + 'description': 'Search http://music.163.com for synchronized lyrics', + 'author': 'ronie', + 'priority': '120', + 'syncronized': True, + 'artist': 'Madonna', + 'title': 'Vogue', +} + +# -a Rainmakers -n Doomsville, for example, reports author only which +# is incomplete and stops the search so I need to move it last. +# Simply comment this if you prefer the above 120 from CU LRC +# -twitham, 2024/01 +info['priority'] = '500' if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/musixmatch.py b/mythtv/programs/scripts/metadata/Music/lyrics/musixmatch.py index 896163b5c07..2713683fac9 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/musixmatch.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/musixmatch.py @@ -1,201 +1,18 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -""" -Scraper for https://www.musixmatch.com - -taxigps -""" - -import os -import requests -import re -import random -import difflib -from bs4 import BeautifulSoup - -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and 'ronie'" -__title__ = "Musixmatch" -__description__ = "Search https://www.musixmatch.com for lyrics" -__priority__ = "210" -__version__ = "0.1" -__syncronized__ = False - -debug = 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 ): - self.SEARCH_URL = 'https://www.musixmatch.com/search/' - self.LYRIC_URL = 'https://www.musixmatch.com' - - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - artist = lyrics.artist.replace(' ', '+') - title = lyrics.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 False - 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 False - elif len(links) > 1: - lyrics.list = links - for link in links: - lyr = self.get_lyrics_from_list(link) - if lyr: - lyrics.lyrics = lyr - return True - return False - - def get_lyrics_from_list(self, link): - title,url,artist,song = link - try: - utilities.log(debug, '%s: search url: %s' % (__title__, url)) - 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 - - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'musixmatch.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.musixmatch.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'Musixmatch', + 'description': 'Search https://musixmatch.com for lyrics', + 'author': 'ronie', + 'priority': '210', + 'syncronized': False, + 'artist': 'Kate Bush', + 'title': 'Wuthering Heights', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/musixmatchlrc.py b/mythtv/programs/scripts/metadata/Music/lyrics/musixmatchlrc.py index 14d8abbc6f6..cd9109d7298 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/musixmatchlrc.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/musixmatchlrc.py @@ -1,228 +1,22 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -""" -Scraper for https://www.musixmatch.com/ - -ronie -https://github.com/rtcq/syncedlyrics -""" - -import requests -import json -import time -import difflib - -import os -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and ronie" -__title__ = "MusixMatchLRC" -__description__ = "Search http://musixmatch.com for lyrics" -__priority__ = "100" -__version__ = "0.1" -__syncronized__ = True - -debug = False - -class LyricsFetcher: - def __init__( self ): - 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(utilities.getCacheDir(), 'musixmatch_token') - if os.path.exists(tokenpath): - tokenfile = open(tokenpath, 'r') - 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 = open(tokenpath, 'w') - json.dump(tokendata, tokenfile) - tokenfile.close() - return self.token - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - self.token = self.get_token() - if not self.token: - return False - artist = lyrics.artist.replace(' ', '+') - title = lyrics.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 False - 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 False - elif len(links) > 1: - lyrics.list = links - for link in links: - lyr = self.get_lyrics_from_list(link) - if lyr: - lyrics.lyrics = lyr - return True - return False - - def get_lyrics_from_list(self, link): - title,trackid,artist,song = link - try: - utilities.log(debug, '%s: search track id: %s' % (__title__, trackid)) - 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 - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Dire Straits' - lyrics.album = 'Brothers In Arms' - lyrics.title = 'Money For Nothing' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'musixmatchlrc.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap + +# In this grabber we need to point its PROFILE to mythtv's cache +# directory, like this: +import lib.culrcscrapers.musixmatchlrc.lyricsScraper +lib.culrcscrapers.musixmatchlrc.lyricsScraper.PROFILE = culrcwrap.getCacheDir() +# make sure this--^^^^^^^^^^^^^ matches this file's basename + +info = { + 'name': '*Musixmatchlrc', + 'description': 'Search https://musixmatch.com for synchronized lyrics', + 'author': 'ronie', + 'priority': '100', + 'syncronized': True, + 'artist': 'Kate Bush', + 'title': 'Wuthering Heights', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, lib.culrcscrapers.musixmatchlrc.lyricsScraper.LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/supermusic.py b/mythtv/programs/scripts/metadata/Music/lyrics/supermusic.py index 8391969f2e8..549adabaf75 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/supermusic.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/supermusic.py @@ -1,181 +1,18 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -""" -Scraper for https://supermusic.cz - -Jose Riha -""" - -import re -import requests -import html - -import os -import sys -from optparse import OptionParser -from common import utilities - -__author__ = "Paul Harrison and Jose Riha" -__title__ = "SuperMusic" -__description__ = "Search https://supermusic.cz for lyrics" -__priority__ = "250" -__version__ = "0.1" -__syncronized__ = False - -debug = False - -class LyricsFetcher: - def __init__( self ): - return - - def get_lyrics(self, lyrics): - utilities.log(debug, "%s: searching lyrics for %s - %s - %s" % (__title__, lyrics.artist, lyrics.album, lyrics.title)) - - artist = lyrics.artist.lower() - title = lyrics.title.lower() - - try: - req = requests.post('https://supermusic.cz/najdi.php', data={'hladane': title, 'typhladania': 'piesen', 'fraza': 'off'}) - response = req.text - except: - return False - 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 True - except: - return False - -def performSelfTest(): - found = False - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - lyrics.artist = 'Karel Gott' - lyrics.album = '' - lyrics.title = 'Trezor' - - fetcher = LyricsFetcher() - found = fetcher.get_lyrics(lyrics) - - if found: - utilities.log(True, "Everything appears in order.") - buildLyrics(lyrics) - sys.exit(0) - - utilities.log(True, "The lyrics for the test search failed!") - sys.exit(1) - -def buildLyrics(lyrics): - from lxml import etree - xml = etree.XML(u'') - etree.SubElement(xml, "artist").text = lyrics.artist - etree.SubElement(xml, "album").text = lyrics.album - etree.SubElement(xml, "title").text = lyrics.title - etree.SubElement(xml, "syncronized").text = 'True' if __syncronized__ else 'False' - etree.SubElement(xml, "grabber").text = lyrics.source - - lines = lyrics.lyrics.splitlines() - for line in lines: - etree.SubElement(xml, "lyric").text = line - - utilities.log(True, utilities.convert_etree(etree.tostring(xml, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def buildVersion(): - from lxml import etree - version = etree.XML(u'') - etree.SubElement(version, "name").text = __title__ - etree.SubElement(version, "author").text = __author__ - etree.SubElement(version, "command").text = 'supermusic.py' - etree.SubElement(version, "type").text = 'lyrics' - etree.SubElement(version, "description").text = __description__ - etree.SubElement(version, "version").text = __version__ - etree.SubElement(version, "priority").text = __priority__ - etree.SubElement(version, "syncronized").text = 'True' if __syncronized__ else 'False' - - utilities.log(True, utilities.convert_etree(etree.tostring(version, encoding='UTF-8', - pretty_print=True, xml_declaration=True))) - sys.exit(0) - -def main(): - global debug - - parser = OptionParser() - - parser.add_option('-v', "--version", action="store_true", default=False, - dest="version", help="Display version and author") - parser.add_option('-t', "--test", action="store_true", default=False, - dest="test", help="Test grabber with a know good search") - parser.add_option('-s', "--search", action="store_true", default=False, - dest="search", help="Search for lyrics.") - parser.add_option('-a', "--artist", metavar="ARTIST", default=None, - dest="artist", help="Artist of track.") - parser.add_option('-b', "--album", metavar="ALBUM", default=None, - dest="album", help="Album of track.") - parser.add_option('-n', "--title", metavar="TITLE", default=None, - dest="title", help="Title of track.") - parser.add_option('-f', "--filename", metavar="FILENAME", default=None, - dest="filename", help="Filename of track.") - parser.add_option('-d', '--debug', action="store_true", default=False, - dest="debug", help=("Show debug messages")) - - opts, args = parser.parse_args() - - lyrics = utilities.Lyrics() - lyrics.source = __title__ - lyrics.syncronized = __syncronized__ - - if opts.debug: - debug = True - - if opts.version: - buildVersion() - - if opts.test: - performSelfTest() - - if opts.artist: - lyrics.artist = opts.artist - if opts.album: - lyrics.album = opts.album - if opts.title: - lyrics.title = opts.title - if opts.filename: - lyrics.filename = opts.filename - - fetcher = LyricsFetcher() - if fetcher.get_lyrics(lyrics): - buildLyrics(lyrics) - sys.exit(0) - else: - utilities.log(True, "No lyrics found for this track") - sys.exit(1) +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.supermusic.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'Supermusic', + 'description': 'Search https://supermusic.cz for lyrics', + 'author': 'Jose Riha', + 'priority': '250', + 'syncronized': False, + 'artist': 'Karel Gott', + 'title': 'Trezor', +} if __name__ == '__main__': - main() + culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/testlyrics.pl b/mythtv/programs/scripts/metadata/Music/lyrics/testlyrics.pl new file mode 100755 index 00000000000..9b3c27f948f --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/testlyrics.pl @@ -0,0 +1,78 @@ +#!/usr/bin/perl + +# test all the grabbers, by twitham@sbcglobal.net, 2024/01 + +# Similar to CU LRC's lib/scrapertest.py but for MythTV's *.py +# wrappers. This tester adds several additional benefits: + +# -t test grabbers in priority order +# lookup any -a artist and -n title +# stop at first match (default) or -k keep going +# high resolution timing summary +# report lyric lines found in the summary + +use warnings; +use strict; +use Getopt::Long; +use File::Basename; +use Time::HiRes qw(gettimeofday tv_interval); + +my $begin = [gettimeofday]; +my($name, $path, $suffix) = fileparse($0); + +my @opt = qw(k|keepgoing v|version t|test s|search d|debug + a|artist=s b|album=s n|title=s f|filename=s); +my %opt; +my $usage = "usage: $0 [-d] [-k] [-v | -t | -a artist -n title] +-d | --debug extra debugging lines to stderr +-k | --keepgoing don't stop at the first match +-v | --version show scraper version headers +-t | --test lookup known good artist/title +-a | --artist lookup given artist +-n | --title lookup given name/title +"; +GetOptions(\%opt, @opt) or die $usage; +$opt{v} or $opt{t} or $opt{a} and $opt{n} or die $usage; + +chdir $path or die $!; +opendir DIR, '.' or die $!; +my(%pri, %sync); + +# code reading hack works with current info format: +for my $py (grep /\.py$/, readdir DIR) { + if (open FILE, $py) { + while () { + m/'priority':\s*'(\d+)'/ + and $pri{$py} = $1; + m/info.'priority'.\s*=\s*'(\d+)'/ + and $pri{$py} = $1; + m/'syncronized':\s*(\S+)/ + and $sync{$py} = $1; + } + } +} +closedir DIR; + +my(%found, %time, %lines); +@opt = map { $opt{$_} ? ("-$_", $opt{$_}) : () } qw/a b n f v d t/; +for my $py (sort { $pri{$a} <=> $pri{$b} } keys %pri) { + my $begin = [gettimeofday]; + my @cmd = ('python3', $py, @opt); + warn "$pri{$py}\t$sync{$py}\t@cmd\n"; + my $out = ''; + open PIPE, '-|', @cmd; + while () { + $out .= $_; + } + close PIPE; + $lines{$py} = $out =~ s///g; + $lines{$py} ||= 0; + $time{$py} = tv_interval($begin, [gettimeofday]); + $out and print $out; + $out and !$opt{k} and last; +} +warn join("\t", qw(pri sync lyrics seconds command)), "\n"; +for my $py (sort { $pri{$a} <=> $pri{$b} } keys %lines) { + warn "$pri{$py}\t$sync{$py}\t$lines{$py}\t$time{$py}\t$py\n"; +} +warn "TOTAL seconds elapsed\t", tv_interval($begin, [gettimeofday]), "\n"; From 3d1e085c3057ef4753689973db3e842929c555f6 Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Sun, 1 Sep 2024 00:27:43 -0500 Subject: [PATCH 3/7] Upgrade to CU LRC 6.6.7 which adds RCLyricsBand and fixes others --- .../scripts/metadata/Music/lyrics/.gitignore | 2 + .../scripts/metadata/Music/lyrics/README | 5 +- .../metadata/Music/lyrics/changelog.txt | 20 +++++ .../metadata/Music/lyrics/common/culrcwrap.py | 2 +- .../culrcscrapers/lyricscom/lyricsScraper.py | 7 +- .../culrcscrapers/lyricsify/lyricsScraper.py | 28 ++++++- .../culrcscrapers/megalobiz/lyricsScraper.py | 2 +- .../culrcscrapers/musixmatch/lyricsScraper.py | 32 +++++++- .../musixmatchlrc/lyricsScraper.py | 2 +- .../rclyricsband/lyricsScraper.py | 75 +++++++++++++++++++ .../metadata/Music/lyrics/lib/scrapertest.py | 44 ++++++++--- .../metadata/Music/lyrics/lib/utils.py | 5 +- .../metadata/Music/lyrics/lyricsify.py | 4 +- .../metadata/Music/lyrics/rclyricsband.py | 18 +++++ 14 files changed, 220 insertions(+), 26 deletions(-) create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py create mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/.gitignore b/mythtv/programs/scripts/metadata/Music/lyrics/.gitignore index 0164cc0d587..1ddb500e261 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/.gitignore +++ b/mythtv/programs/scripts/metadata/Music/lyrics/.gitignore @@ -3,6 +3,8 @@ addon.xml default.py README.txt resources +lib/gui.py +lib/sync.py # twitham's extra files not needed: filetweak.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/README b/mythtv/programs/scripts/metadata/Music/lyrics/README index 169fdea34e5..9c0d2b2a60a 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/README +++ b/mythtv/programs/scripts/metadata/Music/lyrics/README @@ -24,6 +24,7 @@ FileLyrics 90 Yes/No musixmatchlrc 100 Yes NEW in v34 lrclib 110 Yes NEW in v34 lyricsify 130 Yes NEW in v34 +rclyricsband 140 Yes NEW in v35 2024/08 genius 200 No musixmatch 210 No NEW in v34 lyricsmode 220 No @@ -31,7 +32,7 @@ azlyrics 230 No NEW in v34 lyricscom 240 No supermusic 250 No NEW in v34 darklyrics 260 No -megalobiz 400 Yes (too slow to be earlier, was 140) +megalobiz 400 Yes (too slow to be earlier, was 150) music163 500 Yes (sometimes returns author only, was 120) @@ -52,7 +53,7 @@ Simply overwrite lib/ with the latest from the source: 2 of addon.xml in the source and update the latest version number into the header of ./common/culrcwrap.py -* ./testlyrics -k -t +* ./testlyrics.pl -k -t That's all! diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt b/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt index 50c2e60ade1..3a256fa672b 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt +++ b/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt @@ -1,3 +1,23 @@ +v6.6.7 +- fixed RCLyricsBand scraper + +v6.6.6 +- fixed several scrapers + +v6.6.5 +- added "%A - %B - %N - %T" file format + +v6.6.4 +- added "%N. %A - %T" file format + +v6.6.3 +- fix issue with empty ListItem.Property(part1) +- fix for spotify (and others) not in sync +- don't search online sources for saved lyrics files + +v6.6.2 +- improve megalobiz scraper + v6.6.1 - add supermusic scraper diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py index da62d2f29c0..ee1c6ace33d 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py @@ -7,7 +7,7 @@ # UPDATE THIS from https://gitlab.com/ronie/script.cu.lrclyrics # second line of its addon.xml: -__version__ = '6.6.2' +__version__ = '6.6.7' # simulate kodi/xbmc via very simplified Kodistubs. import os 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 index a04720c1cce..6b2dacab99d 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/lyricsScraper.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricscom/lyricsScraper.py @@ -10,6 +10,7 @@ __priority__ = '240' __lrc__ = False +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): @@ -25,7 +26,7 @@ def get_lyrics(self, song): lyrics.source = __title__ lyrics.lrc = __lrc__ try: - request = sess.get(self.url % urllib.parse.quote_plus(song.artist), timeout=10) + request = sess.get(self.url % urllib.parse.quote_plus(song.artist), headers=UserAgent, timeout=10) response = request.text except: return @@ -37,7 +38,7 @@ def get_lyrics(self, song): break if url: try: - req = sess.get(url, timeout=10) + req = sess.get(url, headers=UserAgent, timeout=10) resp = req.text except: return @@ -49,7 +50,7 @@ def get_lyrics(self, song): break if url: try: - req2 = sess.get(url, timeout=10) + req2 = sess.get(url, headers=UserAgent, timeout=10) resp2 = req2.text except: return 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 index dba13e3dd9e..686709db92d 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py @@ -13,8 +13,9 @@ __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"} +UserAgent = {"Host": "www.lyricsify.com", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br, zstd", "DNT": "1", "Alt-Used": "www.lyricsify.com", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Priority": "u=1"} +# lyricsify uses captcha's & cloudflare protection for the search option, only direct lyrics access works class LyricsFetcher: def __init__(self, *args, **kwargs): @@ -23,6 +24,30 @@ def __init__(self, *args, **kwargs): 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("'", '').replace('!', '').replace('?', '').replace('"', '').replace('/', '').replace('.', '').replace('&', '').replace(',', '').replace('(', '').replace(')', '').replace(' ', '-') + title = song.title.replace("'", '').replace('!', '').replace('?', '').replace('"', '').replace('/', '').replace('.', '').replace('&', '').replace(',', '').replace('(', '').replace(')', '').replace(' ', '-') + url = self.SEARCH_URL % (artist.lower(), title.lower()) + 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('details">(.*?)', '', lyricscode) + lyrics.lyrics = lyr + return lyrics + return None + +''' 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) @@ -73,3 +98,4 @@ def get_lyrics_from_list(self, link): lyricscode = (matchcode.group(1)) cleanlyrics = re.sub('<[^<]+?>', '', lyricscode) return cleanlyrics +''' diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py index 2a90ee2bfb0..43c8b21ffc2 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py @@ -13,7 +13,7 @@ from lib.utils import * __title__ = "Megalobiz" -__priority__ = '140' +__priority__ = '150' __lrc__ = True 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 index 60e6e36b8fb..1b1aa1f3c44 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/lyricsScraper.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatch/lyricsScraper.py @@ -10,6 +10,7 @@ import re import random import difflib +import html from bs4 import BeautifulSoup from lib.utils import * @@ -20,14 +21,40 @@ headers = {} headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0' +# search is not possible as it requires javascript, only direct access to the lyrics work. 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' + self.SEARCH_URL = 'https://www.musixmatch.com/search?query=' + self.LYRIC_URL = 'https://www.musixmatch.com/lyrics/%s/%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("'", '').replace('!', '').replace('?', '').replace('"', '').replace('/', '').replace('.', '').replace('&', '').replace(',', '').replace('(', '').replace(')', '').replace(' ', '-') + title = song.title.replace("'", '').replace('!', '').replace('?', '').replace('"', '').replace('/', '').replace('.', '').replace('&', '').replace(',', '').replace('(', '').replace(')', '').replace(' ', '-') + url = self.LYRIC_URL % (artist, title) + try: + log('%s: search url: %s' % (__title__, url), debug=self.DEBUG) + search = requests.get(url, headers=headers, timeout=10) + response = search.text + except: + return None + matchcode = re.search('Lyrics of (.*?)Writer\(s\): ', response, flags=re.DOTALL) + if matchcode: + lyricscode = (matchcode.group(1)) + lyr = re.sub('<[^<]+?>', '\n', lyricscode) + lyr = html.unescape(lyr) + lyrics.lyrics = lyr.replace('\n\n\n\n', '\n') + return lyrics + return None + +''' 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) @@ -84,3 +111,4 @@ def get_lyrics_from_list(self, link): for part in lyr: lyrics = lyrics + part.get_text() + '\n' return lyrics +''' 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 index 783f818e8ce..74a7598ade2 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/lyricsScraper.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/musixmatchlrc/lyricsScraper.py @@ -78,7 +78,7 @@ def get_lyrics(self, song): 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)] + query = [('q', search), ('page_size', '5'), ('page', '1'), ('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: diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py new file mode 100644 index 00000000000..bfd33e796d7 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py @@ -0,0 +1,75 @@ +#-*- coding: utf-8 -*- +''' +Scraper for https://www.rclyricsband.com/ +''' + +import requests +import re +import difflib +import html +from bs4 import BeautifulSoup +from lib.utils import * + +__title__ = "RCLyricsBand" +__priority__ = '140' +__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://rclyricsband.com/' + self.LYRIC_URL = 'https://rclyricsband.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 + title = song.title + try: + url = self.SEARCH_URL + searchdata = {} + searchdata['search'] = '%s %s' % (artist, title) + search = requests.post(url, data=searchdata, headers=UserAgent, timeout=10) + response = search.text + except: + return None + links = [] + soup = BeautifulSoup(response, 'html.parser') + for link in soup.find_all('a', {'class': 'song_search'}): + if link.string: + foundsong = link.string.split(' - ')[0] + foundartist = link.string.split(' - ')[-1] + 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("lrc_text_format'>(.*?)', '\n', lyricscode) + cleanlyrics = html.unescape(cleanlyrics) + return cleanlyrics diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py index 9b64c98414f..2ced98b976b 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py @@ -12,6 +12,7 @@ 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.rclyricsband import lyricsScraper as lyricsScraper_rclyricsband from lib.culrcscrapers.supermusic import lyricsScraper as lyricsScraper_supermusic FAILED = [] @@ -47,7 +48,7 @@ def test_scrapers(): return # test darklyrics - dialog.update(8, LANGUAGE(32163) % 'darklyrics') + dialog.update(7, LANGUAGE(32163) % 'darklyrics') log('==================== darklyrics ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Neurosis' @@ -66,7 +67,7 @@ def test_scrapers(): return # test genius - dialog.update(16, LANGUAGE(32163) % 'genius') + dialog.update(14, LANGUAGE(32163) % 'genius') log('==================== genius ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Maren Morris' @@ -85,7 +86,7 @@ def test_scrapers(): return # test lrclib - dialog.update(24, LANGUAGE(32163) % 'lrclib') + dialog.update(21, LANGUAGE(32163) % 'lrclib') log('==================== lrclib ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'CHVRCHES' @@ -104,7 +105,7 @@ def test_scrapers(): return # test lyricscom - dialog.update(32, LANGUAGE(32163) % 'lyricscom') + dialog.update(28, LANGUAGE(32163) % 'lyricscom') log('==================== lyricscom ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Blur' @@ -123,11 +124,11 @@ def test_scrapers(): return # test lyricsify - dialog.update(40, LANGUAGE(32163) % 'lyricsify') + dialog.update(35, LANGUAGE(32163) % 'lyricsify') log('==================== lyricsify ====================', debug=True) song = Song(opt=lyricssettings) - song.artist = 'Madonna' - song.title = 'Crazy For You' + song.artist = 'Tears For Fears' + song.title = 'Shout' st = time.time() lyrics = lyricsScraper_lyricsify.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) ft = time.time() @@ -142,7 +143,7 @@ def test_scrapers(): return # test lyricsmode - dialog.update(48, LANGUAGE(32163) % 'lyricsmode') + dialog.update(42, LANGUAGE(32163) % 'lyricsmode') log('==================== lyricsmode ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Maren Morris' @@ -161,7 +162,7 @@ def test_scrapers(): return # test megalobiz - dialog.update(56, LANGUAGE(32163) % 'megalobiz') + dialog.update(50, LANGUAGE(32163) % 'megalobiz') log('==================== megalobiz ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Michael Jackson' @@ -180,7 +181,7 @@ def test_scrapers(): return # test music163 - dialog.update(64, LANGUAGE(32163) % 'music163') + dialog.update(58, LANGUAGE(32163) % 'music163') log('==================== music163 ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Madonna' @@ -199,7 +200,7 @@ def test_scrapers(): return # test musixmatch - dialog.update(72, LANGUAGE(32163) % 'musixmatch') + dialog.update(66, LANGUAGE(32163) % 'musixmatch') log('==================== musixmatch ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Kate Bush' @@ -218,7 +219,7 @@ def test_scrapers(): return # test musixmatchlrc - dialog.update(80, LANGUAGE(32163) % 'musixmatchlrc') + dialog.update(73, LANGUAGE(32163) % 'musixmatchlrc') log('==================== musixmatchlrc ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Kate Bush' @@ -236,6 +237,25 @@ def test_scrapers(): if dialog.iscanceled(): return + # test rclyricsband + dialog.update(80, LANGUAGE(32163) % 'rclyricsband') + log('==================== rclyricsband ====================', debug=True) + song = Song(opt=lyricssettings) + song.artist = 'Taylor Swift' + song.title = 'The Archer' + st = time.time() + lyrics = lyricsScraper_rclyricsband.LyricsFetcher(settings=lyricssettings, debug=True).get_lyrics(song) + ft = time.time() + tt = ft - st + TIMINGS.append(['rclyricsband',tt]) + if lyrics: + log(lyrics.lyrics, debug=True) + else: + FAILED.append('rclyricsband') + log('FAILED: rclyricsband', debug=True) + if dialog.iscanceled(): + return + # test supermusic dialog.update(88, LANGUAGE(32163) % 'supermusic') log('==================== supermusic ====================', debug=True) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py index 03f30cc3d48..582485f3bcd 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/utils.py @@ -68,7 +68,7 @@ def get_artist_from_filename(*args, **kwargs): 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: + elif SETTING_READ_FILENAME_FORMAT in (3,5): at = basename.split(' ', 1)[1].strip() artist = at.split('-', 1)[0].strip() title = os.path.splitext(at.split('-', 1)[1].strip())[0] @@ -76,6 +76,9 @@ def get_artist_from_filename(*args, **kwargs): elif SETTING_READ_FILENAME_FORMAT == 4: artist = basename.split('-', 2)[1].strip() title = os.path.splitext(basename.split('-', 2)[2].strip())[0] + elif SETTING_READ_FILENAME_FORMAT == 6: + artist = basename.split('-', 1)[0].strip() + title = os.path.splitext(basename.split('-', 3)[3].strip())[0] except: # invalid format selected log('failed to get artist and title from filename', debug=DEBUG) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py b/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py index 6b96885e910..be21e11af47 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py @@ -10,8 +10,8 @@ 'author': 'ronie', 'priority': '130', 'syncronized': True, - 'artist': 'Madonna', - 'title': 'Crazy For You', + 'artist': 'Tears For Fears', + 'title': 'Shout', } if __name__ == '__main__': diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py b/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py new file mode 100644 index 00000000000..18295cf2000 --- /dev/null +++ b/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py @@ -0,0 +1,18 @@ +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- + +from common import culrcwrap +from lib.culrcscrapers.rclyricsband.lyricsScraper import LyricsFetcher +# make sure this-------^^^^^^^^^ matches this file's basename + +info = { + 'name': 'RCLyricsBand', + 'description': 'Search http://www.rclyricsband.com for lyrics', + 'author': 'ronie', + 'priority': '140', + 'syncronized': True, + 'artist': 'Taylor Swift', + 'title': 'The Archer', +} + +if __name__ == '__main__': + culrcwrap.main(__file__, info, LyricsFetcher) From b88b9e8f660e91eb7c7caa8d7ba3ff2596c535f7 Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Thu, 5 Sep 2024 18:41:49 -0500 Subject: [PATCH 4/7] Upgrade to CU LRC 6.6.8 which drops broken lyricsify --- .../scripts/metadata/Music/lyrics/README | 5 ++- .../metadata/Music/lyrics/changelog.txt | 4 +++ .../metadata/Music/lyrics/common/culrcwrap.py | 2 +- .../lyricsify/lyricsScraper.py | 0 .../culrcscrapers/megalobiz/lyricsScraper.py | 2 +- .../rclyricsband/lyricsScraper.py | 6 ++-- .../metadata/Music/lyrics/lib/scrapertest.py | 32 ++++--------------- .../metadata/Music/lyrics/lyricsify.py | 18 ----------- .../metadata/Music/lyrics/rclyricsband.py | 2 +- 9 files changed, 18 insertions(+), 53 deletions(-) rename mythtv/programs/scripts/metadata/Music/lyrics/lib/{culrcscrapers => broken-scrapers}/lyricsify/lyricsScraper.py (100%) delete mode 100644 mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/README b/mythtv/programs/scripts/metadata/Music/lyrics/README index 9c0d2b2a60a..0933e618aa0 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/README +++ b/mythtv/programs/scripts/metadata/Music/lyrics/README @@ -23,8 +23,7 @@ EmbeddedLyrics 50 Yes/No FileLyrics 90 Yes/No musixmatchlrc 100 Yes NEW in v34 lrclib 110 Yes NEW in v34 -lyricsify 130 Yes NEW in v34 -rclyricsband 140 Yes NEW in v35 2024/08 +rclyricsband 130 Yes NEW in v35 2024/08 genius 200 No musixmatch 210 No NEW in v34 lyricsmode 220 No @@ -32,7 +31,7 @@ azlyrics 230 No NEW in v34 lyricscom 240 No supermusic 250 No NEW in v34 darklyrics 260 No -megalobiz 400 Yes (too slow to be earlier, was 150) +megalobiz 400 Yes (too slow to be earlier, was 140) music163 500 Yes (sometimes returns author only, was 120) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt b/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt index 3a256fa672b..8f71756f5a1 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt +++ b/mythtv/programs/scripts/metadata/Music/lyrics/changelog.txt @@ -1,3 +1,7 @@ +v6.6.8 +- remove broken lyricsify scraper +- cleanup RCLyricsBand lyrics + v6.6.7 - fixed RCLyricsBand scraper diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py b/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py index ee1c6ace33d..63887af3c2e 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/common/culrcwrap.py @@ -7,7 +7,7 @@ # UPDATE THIS from https://gitlab.com/ronie/script.cu.lrclyrics # second line of its addon.xml: -__version__ = '6.6.7' +__version__ = '6.6.8' # simulate kodi/xbmc via very simplified Kodistubs. import os diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricsify/lyricsScraper.py similarity index 100% rename from mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/lyricsify/lyricsScraper.py rename to mythtv/programs/scripts/metadata/Music/lyrics/lib/broken-scrapers/lyricsify/lyricsScraper.py diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py index 43c8b21ffc2..2a90ee2bfb0 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/megalobiz/lyricsScraper.py @@ -13,7 +13,7 @@ from lib.utils import * __title__ = "Megalobiz" -__priority__ = '150' +__priority__ = '140' __lrc__ = True diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py index bfd33e796d7..13ed87c2ccb 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/culrcscrapers/rclyricsband/lyricsScraper.py @@ -1,17 +1,17 @@ -#-*- coding: utf-8 -*- +#-*- coding: UTF-8 -*- ''' Scraper for https://www.rclyricsband.com/ ''' import requests import re -import difflib import html +import difflib from bs4 import BeautifulSoup from lib.utils import * __title__ = "RCLyricsBand" -__priority__ = '140' +__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"} diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py b/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py index 2ced98b976b..b7aecbcddb5 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/lib/scrapertest.py @@ -6,7 +6,6 @@ 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 @@ -48,7 +47,7 @@ def test_scrapers(): return # test darklyrics - dialog.update(7, LANGUAGE(32163) % 'darklyrics') + dialog.update(8, LANGUAGE(32163) % 'darklyrics') log('==================== darklyrics ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Neurosis' @@ -67,7 +66,7 @@ def test_scrapers(): return # test genius - dialog.update(14, LANGUAGE(32163) % 'genius') + dialog.update(16, LANGUAGE(32163) % 'genius') log('==================== genius ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Maren Morris' @@ -86,7 +85,7 @@ def test_scrapers(): return # test lrclib - dialog.update(21, LANGUAGE(32163) % 'lrclib') + dialog.update(24, LANGUAGE(32163) % 'lrclib') log('==================== lrclib ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'CHVRCHES' @@ -105,7 +104,7 @@ def test_scrapers(): return # test lyricscom - dialog.update(28, LANGUAGE(32163) % 'lyricscom') + dialog.update(32, LANGUAGE(32163) % 'lyricscom') log('==================== lyricscom ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Blur' @@ -123,27 +122,8 @@ def test_scrapers(): if dialog.iscanceled(): return - # test lyricsify - dialog.update(35, LANGUAGE(32163) % 'lyricsify') - log('==================== lyricsify ====================', debug=True) - song = Song(opt=lyricssettings) - song.artist = 'Tears For Fears' - song.title = 'Shout' - 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(42, LANGUAGE(32163) % 'lyricsmode') + dialog.update(40, LANGUAGE(32163) % 'lyricsmode') log('==================== lyricsmode ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Maren Morris' @@ -162,7 +142,7 @@ def test_scrapers(): return # test megalobiz - dialog.update(50, LANGUAGE(32163) % 'megalobiz') + dialog.update(49, LANGUAGE(32163) % 'megalobiz') log('==================== megalobiz ====================', debug=True) song = Song(opt=lyricssettings) song.artist = 'Michael Jackson' diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py b/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py deleted file mode 100644 index be21e11af47..00000000000 --- a/mythtv/programs/scripts/metadata/Music/lyrics/lyricsify.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil; -*- - -from common import culrcwrap -from lib.culrcscrapers.lyricsify.lyricsScraper import LyricsFetcher -# make sure this-------^^^^^^^^^ matches this file's basename - -info = { - 'name': '*Lyricsify', - 'description': 'Search https://lyricsify.com for synchronized lyrics', - 'author': 'ronie', - 'priority': '130', - 'syncronized': True, - 'artist': 'Tears For Fears', - 'title': 'Shout', -} - -if __name__ == '__main__': - culrcwrap.main(__file__, info, LyricsFetcher) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py b/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py index 18295cf2000..59739987e52 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py @@ -8,7 +8,7 @@ 'name': 'RCLyricsBand', 'description': 'Search http://www.rclyricsband.com for lyrics', 'author': 'ronie', - 'priority': '140', + 'priority': '130', 'syncronized': True, 'artist': 'Taylor Swift', 'title': 'The Archer', From fd87c445dac76455d7016e668550050d9524181f Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Fri, 6 Sep 2024 23:36:44 -0500 Subject: [PATCH 5/7] Update CMake list to current lyrics/*.py --- .../metadata/Music/lyrics/CMakeLists.txt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt b/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt index 0631c6f56fa..d2625eb1864 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt +++ b/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt @@ -9,20 +9,20 @@ # set(PYFILES - azlyrics.py - darklyrics.py - embedlrc.py - filelyrics.py - genius.py - lrclib.py - lyricscom.py - lyricsify.py - lyricsmode.py - megalobiz.py - music163.py - musixmatch.py - musixmatchlrc.py - supermusic.py) + azlyrics.py + darklyrics.py + embedlrc.py + filelyrics.py + genius.py + lrclib.py + lyricscom.py + lyricsmode.py + megalobiz.py + music163.py + musixmatchlrc.py + musixmatch.py + rclyricsband.py + supermusic.py) # # The cmake documentation strongly recommands against globbing source file From 5ac95d510d1d45cd5075bce28e8faf3468b5358d Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Sat, 7 Sep 2024 01:26:49 -0500 Subject: [PATCH 6/7] Add * before RCLyricsBand name, sorts first and indicates syncronized on the menu --- mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py b/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py index 59739987e52..a8bb4688180 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py +++ b/mythtv/programs/scripts/metadata/Music/lyrics/rclyricsband.py @@ -5,7 +5,7 @@ # make sure this-------^^^^^^^^^ matches this file's basename info = { - 'name': 'RCLyricsBand', + 'name': '*RCLyricsBand', 'description': 'Search http://www.rclyricsband.com for lyrics', 'author': 'ronie', 'priority': '130', From c5e600c0d30ed731c7db985004a2a941efc19087 Mon Sep 17 00:00:00 2001 From: Timothy D Witham Date: Sun, 8 Sep 2024 13:59:38 -0500 Subject: [PATCH 7/7] cmake must install lib/ and Kodistubs/ --- mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt b/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt index d2625eb1864..9b9aa0dd7b4 100644 --- a/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt +++ b/mythtv/programs/scripts/metadata/Music/lyrics/CMakeLists.txt @@ -42,6 +42,6 @@ add_custom_target(metadata_Music_lyrics ALL DEPENDS ${PROCESSED_FILES}) install(FILES README DESTINATION ${CMAKE_INSTALL_DATADIR}/mythtv/metadata/Music/lyrics) install( - DIRECTORY common examples + DIRECTORY common examples lib Kodistubs DESTINATION ${CMAKE_INSTALL_DATADIR}/mythtv/metadata/Music/lyrics PATTERN "\.gitignore" EXCLUDE)