diff --git a/screensaver.picture.slideshow/LICENSE.txt b/screensaver.picture.slideshow/LICENSE.txt deleted file mode 100644 index 4f8e8eb30..000000000 --- a/screensaver.picture.slideshow/LICENSE.txt +++ /dev/null @@ -1,282 +0,0 @@ - - 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/screensaver.picture.slideshow/addon.xml b/screensaver.picture.slideshow/addon.xml deleted file mode 100644 index ec2aa2293..000000000 --- a/screensaver.picture.slideshow/addon.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - - Sluimerskerm wat prente vertoon as skyfievertoning - መመልከቻ ማዳኛ የ ምስል ተንሸራታች ማሳያ - شاشة التوقف هي التي تعرض صورة تلقائيا - Ахоўнік экрану што робіць паказ слайдаў - Скрийнсейвър, изобразяващ презентация от картини - Estalvi de pantalla que mostra una presentació de diapositives d'imatges - Spořič obrazovky promítající obrázky - Arbedwr sgrin sy'n dangos sioe o ddelweddau - Pauseskærm som afspiller et diasshow - Bildschirmschoner, der eine Bilder-Diashow anzeigt - Μία προφύλαξη οθόνης παρουσίασης διαφανειών - Screensaver that displays an image slideshow - Screensaver that displays an image slideshow - Screensaver that displays an image slideshow - Un salvapantallas que muestra una presentación de imágenes - Un protector de pantalla que muestra una presentación de imágenes - Protector de pantalla que muestra una presentación de imágenes - Ekraanisäästja, mis näitab piltide slaidiesitlust - Näytönsäästäjä, joka toistaa kuvia diaesityksenä - Économiseur d'écran affichant un diaporama - Économiseur d'écran affichant un diaporama - O gardapantallas que amosa unha presentación de imaxes - שומר מסך אשר יציג מצגת של תמונות - सक्रीनसेवर जो चित्रो के स्लीडेशो को प्रदर्शन करता है - Čuvar zaslona koji prikazuje slikovnu prezentaciju - Képernyővédő ami diavetítést csinál - Skjáhvíla sem sýnir skyggnusýningu mynda - Salvaschermo che mostra una presentazione di immagini - 画像をスライドショー表示するスクリーンセーバー - 이미지 슬라이드쇼를 보여주는 화면 보호기 - Ekrano užsklanda, kuri rodo skaidrių peržiūrą - Ekrānsaudzētājs, kas rāda attēlu slīdīti - Заштита на екран што покажува слајдшоу од слики - Penyelamat skrin yang memaparkan paparan slaid imej - Screensaver သည်ပုံများကို slideshow အဖြစ်ပြသပေးသည်။ - Schermbeveiliging die een diavoorstelling toont - Skjermbeskytter med lysbildefremvisning - Wygaszacz ekranu wyświetlający pokaz slajdów - Protecção de ecrã que mostra uma apresentação de slides - Proteção de tela que mostra uma apresentação de fotografias - Protector ecran care afișează o diaporamă de imagini - Заставка, показывающая слайд-шоу из изображений - Šetrič obrazovky zobrazujúci prezentáciu obrázkov - Ohranjevalnik zaslona, ki prikazuje diaprojekcijo slik - Ruajtës ekrani, që paraqet një shfaqje në seri ‘slideshow’ të imazheve - En skärmsläckare som visar ett bildspel - படகாட்சி திரைகாப்பன் - Resim slayt şovu gösteren ekran koruyucu - Заставка що відображує слайдшоу із зображень. - Trình bảo vệ màn hình giống như một trình chiếu ảnh - 幻灯片屏幕保护程序 - 顯示圖片幻燈秀的螢幕保護程式 - Die Skyfievertoning sluimerskerm wys jou 'n skyfie vertoning met verskeie oorgang effekte. Dit kan gestel word om jou biblioteek se musiek of video aanhangerkuns, of 'n persoonlike lêer met prente te vertoon. - የ ተንሸራታች ማሳያ መመልከቻ ማዳኛ የተለያዩ ተንሸራታች ማሳያ ምስሎች በ ተለያዩ መሸጋገሪያ ውጤቶችን በመጠቀም ያሳይዎታል፡ ማዋቀር ይችላሉ የ ሙዚቃ መጻህፍት ቤት ወይንም የ ቪዲዮ ወይንም የምስሎች ፎልደር እንዲያሳይ - شاشة توقف العرض التلقائي ستظهر لك عرض تدريجي للصور باستخدام تأثيرات تحول مختلفة. ممكن أن تكون كونت لتعرض مكتبة الموسيقى أو الفيديو الخاصة بك, أو مجلد مخصص من الصور. - Слайдавы ахоўнік экрану будзе паказваць выявы з дапамогай разнастайных пераходных эфектаў. Можна сканфігураваць для паказу вашых бібліятэк музыкі ці відэа, альбо пэўных каталогаў з выявамі - Скрийнсейвърът „Презентация“ изобразява презентация от картини с разнообразни ефекти при прехода между отделните картини. Можете да зададете като източник на картини фен-арт изображенията от видео/музикалната библиотеката или персонална папка с изображения. - El protector de pantalles Projecció mostrarà una projecció de diapositives utilitzant diferents efectes de transició. Es pot configurar per mostrar el fanart de les vostres biblioteques de música o un directori d'imatges personalitzat - Spořič obrazovky promítání vám bude promítat zvolené obrázky s použitím různých přechodových efektů. Může být nastaven k zobrazování fanart z vašich hudebních, či video knihoven, nebo vlastní složky obrázků. - Bydd yr arbedwr sgrin Slideshow yn dangos sioe o ddelweddau dan ddefnyddio amryw o effeithiau trawsnewidiol.Mae modd ei ffurfweddu i ddangos celf eich cerddoriaeth neu fideo neu ffolder cyfaddas o ddelweddau. - Diasshow-pauseskærmen vil vise et diasshow af billeder ved hjælp af forskellige overgangseffekter. Den kan konfigureres til at vise fankunst fra dit biblioteks musik eller videoer, eller billeder fra en selvvalgt mappe. - Der Slideshow-Bildschirmschoner zeigt eine Bilder-Slideshow mit verschiedenen Übergangseffekten. Er kann so konfiguriert werden, dass Musik oder Video Fanarts aus der Bibliothek, oder Bilder eines bestimmten Ordners angezeigt werden. - Η προφύλαξη οθόνης Slideshow θα εμφανίσει μία σειρά εικόνων χρησιμοποιώντας ένα πλήθος εφέ μετάβασης. Μπορεί να ρυθμιστεί ούτως ώστε να εμφανίζει τις εικόνες fanart της μουσικής ή βίντεο συλλογής σας, ή έναν προσωπικό φάκελο εικόνων - The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images - The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images - The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images - El salvapantallas Slideshow mostrará una presentación de imágenes usando varios efectos de transición. Puede ser configurado para mostrar los fanart de tus bibliotecas de música o video, o las imágenes de una carpeta. - El protector de pantalla Slideshow mostrará una presentación de imágenes usando varios efectos de transición. Puede ser configurado para mostrar los fanart de tus bibliotecas de música o video, o las imágenes de una carpeta. - El protector de pantalla Presentación de Imágenes te mostrará una presentación de imágenes usando varios efectos de transición. Puede ser configurado para mostrar tus librerías de fanart de música o video, o una carpeta de imágenes personalizada. - Slaidiesitluse ekraanisäästja näitab sulle slaidiesitlust kasutades erinevaid üleminekuefekte. Slaidiesitlust on võimalik koostada nii muusika või videote fännipiltidest kui ka kasutades selleks spetsiaalset määratud piltide kausta. - Slideshow-näytönsäästäjä näyttää diaesityksen käyttäen erilaisia siirtymätehosteita. Sen voi asettaa näyttämään musiikki- tai videokirjastojen fanitaidetta tai kuvia valitusta kuvakansiosta. - L'économiseur Diaporama affiche un album photo en utilisant divers effets de transition. Il peut afficher la collection de fanarts vidéos ou musicaux, ou un dossier personnalisé d'images. - L'économiseur d'écran diaporama vous montrera un diaporama d'images avec différents effets de transition. Il peut être configuré pour afficher votre audiothèque, les fanarts de vos vidéos ou un dossier personnalisé d'images. - O gardapantallas Slideshow amosará unha presentación de imaxes empregando varios efectos de transición. Pode ser configurado para amosar os carteis das túas biblotecas de video ou música ou imaxes dun cartafol personalizado. - השומר מסך יציג לך מצגת של תמונות אשר עושה שימוש במעברים שונים. יש אפשרות להגדיר את תמונות הפאנארט של ספריות המוזיקה והוידאו, או תיקיית תמונות אישית - Čuvar zaslona slikovne prezentacije prikazuje slikovnu prezentaciju uz razne prijelazne efekte. Može se podesiti da prikazuje Vašu bazu glazbenih ili video slika omota, ili odabranu mapu sa slikama - A Slideshow képernyővédő diavetítés-szerűen képeket mutat egymás után különböző váltási effektussal. Beállítható úgy hogy a médiatárból mutasson zenei-, vagy film fanartképeket, vagy egy egyedi mappából mutassa azokat. - Þessi skjáhvíla sýnir þér skyggnusýningu af myndum með ýmsum myndbrellum. Það er hægt að stilla hana til að sýna Fanart myndir úr tónlistar eða myndbandasöfnum, eða frá sérsniðinni möppu með myndum. - Il salvaschermo Slideshow mostra una presentazione di immagini usando diversi effetti di transizione. Può essere configurato per mostrare le fanart delle librerie musica o video, oppure una cartella di immagini specifica - The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images - 슬라이드쇼 화면 보호기는 다양한 화면 전환효과를 이용해 이미지 슬라이드쇼를 보여줍니다. 음악 라이브러리, 비디오 팬아트, 사용자 폴더의 사진을 볼 수 있게 설정할 수 있습니다. - Skaidrių peržiūros ekrano užsklanda atvaizduoja skaidrių peržiūros vaizdus, naudojant įvairius perėjimo efektus. Jis konfigūruojamas, kad rodyti jūsų bibliotekos muzikos ar vaizdo FanArt arba pasirinktinį paveikslėlių aplanką - Slīdītes ekrānsaudzētājs parādīs slīdīti ar attēliem, izmantojot dažādus pārejas efektus. To var konfigurēt, lai rāda fanumākslu no mūzikas vai video bibliotekām, vai arī no pielāgotas attēlu mapes - Слајдшоу заштитата на екран ќе покаже слајд шоу на слики користејќи ефекти на транзиција. Може да биде конфигурирано да покажува и плакат од вашата музика или филмови или ваша папка со слики - Penyelamat skrin paparan slaid akan paparkan paparan slaid imej menggunakan pelabgai kesan peralihan. Ia boleh dikonfigur untuk tunjukkan pustaka muzik atau seni peminat anda, atau folder imej suai - De Diavoorstellingsschermbeveiliging zal u een diavoorstelling tonen met verschillende overgangseffecten. U kan muziek- of videofanart uit uw bibliotheek tonen of zelf een map opgeven die afbeeldingen bevat. - Denne skjermbeskytteren viser bilder med forskjellige overgangseffekter. Den kan konfigureres til å vise fanart fra musikk- eller videobiblioteket ditt, eller en mappe med bilder. - Ten wygaszacz ekranu to pokaz slajdów obrazów, przy użyciu różnych efektów przejść. Może być skonfigurowany tak, aby pokazywał fototapety filmów, muzyki lub obrazy z niestandardowego folderu. - O protector de ecrã de slides irá mostrar-lhe imagens usando vários efeitos de transição. Pode ser configurada para mostrar as imagens de Fanart da sua biblioteca de música ou de vídeo, e também pode mostrar as imagens de uma pasta à sua escolha. - O screensaver Slideshow irá mostrar-lhe uma apresentação de slides de suas fotografias usando diferentes efeitos de transição. Ele pode ser configurado para visualizar fanarts de sua biblioteca de músicas ou de vídeo, ou ainda utilizar uma pasta personalizada que contenha imagens - Protector ecran de tip diaporamă care va afișa imagini folosind diferite efecte de tranziție. Poate fi configurat să afișeze imagini creații artistice din mediateca audio sau video sau dintr-un dosar personalizat. - Эта заставка показывается слайд-шоу из изображений, используя различные эффекты переходов. Заставку можно настроить для показ фанарта для музыки и видео из вашей библиотеки мультимедиа или изображениям из заданной папки. - Tento šetrič vám ukáže prezentáciu obrázkov za použitia rôznych efektov prechodov. Môže byť nastavený na zobrazovanie vašej knižnice hudby alebo video fanartov, alebo vlastného priečinku s obrázkami. - Predvajalnik slik prikaže diaprojekcijo slik z uporabo različnih učinkov prehoda. To je mogoče nastaviti tako, da se prikažejo ozadja knjižnice glasbe ali videa alislike iz izbrane mape. - Ruajtësi i ekranit ‘Slideshow’ do t'ju paraqesë një shfaqje në seri të imazheve, duke përdorur efekte të ndryshme tranzicioni. Mund të konfigurohet që të shfaqë sfonde të koleksiont të muzikës apo vidjove tuaja, ose imazhe prej një dosjeje të caktuar - Bildspel är en skärmsläckare som visar ett bildspel med olika övergångseffekter. Den kan ställas in för att visa din musik- eller video-fanart från biblioteket eller en utvald mapp med bilder. - படகாட்சி திரைகாப்பன் பல்வேறு படங்களை பல மாற்ற விளைவுகளை பயன்படுத்தி படகாட்சி செய்யும். இது உங்கள் நூலகங்கள் இசை அல்லது வீடியோ விசிறிபடம் அல்லது படக்கொப்புரை படங்களை காட்சியில் காண்பிக்குமாறு செய்யமுடியும். - Slideshow ekran koruyucu size farklı geçiş efektleriyle resim gösterisi yapar. Müzik kütüphanenizden veya video fanart yada özel bir klasördeki resimlerden seçilebilir. - Заставка Слайдшоу показує зображення по черзі, використовуючи різноманітні ефекти переходу. Заставка може бути налаштована на показ зображень з різних джерел: обкладинок відео фільмів, обкладинок музичних альбомів або будь-яких фото із вказаної користувачем папки. - Trình bảo vệ màn hình Slideshow sẽ giúp bạn trình chiếu bộ sưu tập ảnh với các hiệu ứng chuyển đổi đẹp mắt. Bạn có thể tuỳ chỉnh để hiện thông tin về bài hát, video, fanart hoặc một thư mục chứa ảnh bất kì - 幻灯片屏保将使用各种过渡效果以幻灯片形式显示您的图片。你可以设置它显示音乐或视频资料库同人画,或某个文件夹中的图片。 - 幻燈片屏幕保護程序可以顯示使用各種過場效果的幻燈片播放。它可以被配置為顯示您的圖書館音樂模式或視頻圖庫,或自行定義的文件夾中的圖像。 - all - GPL-2.0-only - https://forum.kodi.tv/showthread.php?tid=154032 - https://gitlab.com/ronie/screensaver.picture.slideshow - - resources/icon.png - resources/fanart.jpg - - - add support for image types provided by imagedecoder addons - - diff --git a/screensaver.picture.slideshow/changelog.txt b/screensaver.picture.slideshow/changelog.txt deleted file mode 100644 index 21c1b5a71..000000000 --- a/screensaver.picture.slideshow/changelog.txt +++ /dev/null @@ -1,286 +0,0 @@ -v7.0.3 -- bump for release - -v7.0.2 -- fix encoding - -v7.0.1 -- add support for image types provided by imagedecoder addons - -v7.0.0 -- update for kodi nexus - -v6.4.2 -- revert some changes due to python/json problem with windows paths -- make recursive slideshow optional - -v6.4.1 -- fix crash when video files are present - -v6.4.0 -- add basic support for picture addons - -v6.3.6 -- add vignette option - -v6.3.5 -- added an option to display full image path - -v6.3.4 -- don't require shares to be writable - -v6.3.3 -- update for kodi 20 - -v6.3.2 -- improve xbmcvfs file read handling - -v6.3.1 -- use official python libraries - -v6.3.0 -- use art table to retrieve fanart -- use json to read and write cache - -v6.2.0 -- convert skin to 1080p -- add support for more picture extensions - -v6.1.0 -- update exif, iptc and xmp data parsers -- fix: display background setting was not selectable -- fix: slideshow with no effect was broken - -v6.0.2 -- cleanup - -v6.0.1 -- bump - -v6.0.0 -- changes for python 3 - -v5.0.9 -- fix splash image kept spinning in the background, eating up cpu - -v5.0.8 -- re-add option to disable animations - -v5.0.4 -- add option to disable animations - -v5.0.3 -- move repo to gitlab - -v5.0.2 --cosmetics -- skin tweaks - -v5.0.1 -- add black background - -v4.2.3 -- don't crash when failing to retrieve skinpath - -v4.2.2 -- extra logging - -v4.2.1 -- cosmetics - -v4.2.0 -- add resume from last position option - -v4.1.4 -- better handling when picture folder is empty - -v4.1.3 -- use image instead of largeimage controls - -v4.1.2 -- don't create empty cache file -- delete empty cache files - -v4.1.1 -- don't crash when advancedsettings.xml is corrupt - -v4.1.0 -- option to exclude dirs/files using pictureexcludes in advancedsettings.xml - -v4.0.1 -- fix cache creation when an empty cache file is present. - -v4.0.0 -- remove simplejson support - -v3.0.3 -- language update -- cosmetics - -v3.0.2 -- fix randomizing the list - -v3.0.1 -- fix index creation during update - -v3.0.0 -- bump for jarvis -- always use image index -- remove option to create an index - -v2.1.9 -- auto placement of exif info labels - -v2.1.8 -- fix background creation of image index - -v2.1.7 -- move music info to bottom - -v2.1.6 -- cosmetics - -v2.1.5 -- use different method to find the addon.xml of the skin - -v2.1.4 -- fix missing settings string -- fix crash on update due to incompatible settings - -v2.1.3 -- language update - -v2.1.2 -- make background image optional - -v2.1.1 -- add background image to reduce black borders - -v2.1.0 -- add update thread - -v2.0.11 -- add option to show both folder- and filename - -v2.0.10 -- updated language files from Transifex - -v2.0.9 -- deprecate xbmc.abortRequested - -v2.0.8 -- add missing images - -v2.0.7 -- fix recursive scan (paths must end with a slash) - -v2.0.6 -- fix update index button - -v2.0.5 -- fix crash on exit - -v2.0.4 -- apply dim value to the whole screen - -v2.0.3 -- more screen space for image filename -- disable caching of images - -v2.0.2 -- change name to screensaver.picture.slideshow - -v2.0.1 -- deprecate previous version - -v2.0.0 -- kodi name change - -v1.0.6 -- increase zorder - -v1.0.5 -- fixed unicode error - -v1.0.4 -- fixed: seekbar dialog would display on top of screensaver - -v1.0.3 -- update language files - -v1.0.2 -- fix album cover missing when playing music - -v1.0.1 -- add xml header - -v1.0.0 -- gotham release - -v0.1.8 -- update language files - -v0.1.7 -- use natural sort order - -v0.1.6 -- fix empty tags handling in xmp library - -v0.1.5 -- auto update cache -- only use cache with image folder source - -v0.1.4 -- fix crash when checking cached images - -v0.1.3 -- add support for xmp tags -- make music info slightly transparent (prevent burn-in) - -v0.1.2 -- added yyyy-mm-dd format - -v0.1.1 -- fix potential issues due to missing module imports - -v0.1.0 -- added option to disable the randomizing of images -- added display name for fanart -- added cache option - -v0.0.9 -- silence iptc warnings -- update code for xbmc 12.1 - -v0.0.8 -- added music info option -- localize date format -- removed iptc info labels - -v0.0.7 -- change picture tags code - -v0.0.6 -- use largeimage controls in skin -- add support for exif tags - -v0.0.5 -- added dependencies to addon.xml - -v0.0.4 -- fixed: previous image would briefly show when switching from the current to the next -- fixed: adjust animation speeds according to the effectslowdown value of the skin -- fixed: pre-load images -- changed: allow images to be displayed up to 60 seonds -- added: option to scale the image fullscreen -- added: option to display the image name -- added: splash screen during startup - -v0.0.3 -- added: multipath support - -v0.0.2 -- added: fanart image - -v0.0.1 -- initial release diff --git a/screensaver.picture.slideshow/default.py b/screensaver.picture.slideshow/default.py deleted file mode 100644 index 372c33898..000000000 --- a/screensaver.picture.slideshow/default.py +++ /dev/null @@ -1,15 +0,0 @@ -import xbmcaddon - -ADDON = xbmcaddon.Addon() -CWD = ADDON.getAddonInfo('path') -ADDONVERSION = ADDON.getAddonInfo('version') - -from lib.utils import * - -if __name__ == '__main__': - log('script version %s started' % ADDONVERSION) - from lib import gui - screensaver_gui = gui.Screensaver('script-python-slideshow.xml', CWD, 'default') - screensaver_gui.doModal() - del screensaver_gui -log('script stopped') diff --git a/screensaver.picture.slideshow/lib/gui.py b/screensaver.picture.slideshow/lib/gui.py deleted file mode 100644 index e1f3f0ee1..000000000 --- a/screensaver.picture.slideshow/lib/gui.py +++ /dev/null @@ -1,516 +0,0 @@ -# * This Program is free software; you can redistribute it and/or modify -# * it under the terms of the GNU General Public License as published by -# * the Free Software Foundation; either version 2, or (at your option) -# * any later version. -# * -# * This Program is distributed in the hope that it will be useful, -# * but WITHOUT ANY WARRANTY; without even the implied warranty of -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# * GNU General Public License for more details. -# * -# * You should have received a copy of the GNU General Public License -# * along with Kodi; see the file COPYING. If not, write to -# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. -# * http://www.gnu.org/copyleft/gpl.html - -import copy -import random -import threading -from xml.dom.minidom import parse -import exifread -from iptcinfo3 import IPTCInfo -import xbmcgui -from lib.utils import * - -ADDON = xbmcaddon.Addon() -SKINDIR = xbmc.getSkinDir() - -# images types that can contain exif/iptc data -EXIF_TYPES = ('.jpg', '.jpeg', '.tif', '.tiff') - -# random effect list to choose from -EFFECTLIST = ["('conditional', 'effect=zoom start=100 end=400 center=auto time=%i condition=true'),", - "('conditional', 'effect=slide start=1920,0 end=-1920,0 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')", - "('conditional', 'effect=slide start=-1920,0 end=1920,0 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')", - "('conditional', 'effect=slide start=0,1080 end=0,-1080 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')", - "('conditional', 'effect=slide start=0,-1080 end=0,1080 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')", - "('conditional', 'effect=slide start=1920,1080 end=-1920,-1080 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')", - "('conditional', 'effect=slide start=-1920,1080 end=1920,-1080 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')", - "('conditional', 'effect=slide start=1920,-1080 end=-1920,1080 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')", - "('conditional', 'effect=slide start=-1920,-1080 end=1920,1080 time=%i condition=true'), ('conditional', 'effect=zoom start=%i end=%i center=auto time=%i condition=true')"] - -# get local dateformat to localize the exif date tag -DATEFORMAT = xbmc.getRegion('dateshort') - - -class BinaryFile(xbmcvfs.File): - def read(self, numBytes: int = 0) -> bytes: - if not numBytes: - return b"" - return bytes(self.readBytes(numBytes)) - - -class Screensaver(xbmcgui.WindowXMLDialog): - def __init__( self, *args, **kwargs ): - pass - - def onInit(self): - # load vars - self._get_vars() - # get addon settings - self._get_settings() - # get the effectslowdown value from the current skin - effectslowdown = self._get_animspeed() - # use default if we couldn't find the effectslowdown value - if not effectslowdown: - effectslowdown = 1 - # calculate the animation time - speedup = 1 / float(effectslowdown) - self.adj_time = int(101000 * speedup) - # get the images - self._get_items() - if self.slideshow_type == 2 and not self.slideshow_random and self.slideshow_resume: - self._get_offset() - if self.items: - # hide startup splash - self._set_prop('Splash', 'hide') - # start slideshow - self._start_show(copy.deepcopy(self.items)) - - def _get_vars(self): - # get the screensaver window id - self.winid = xbmcgui.Window(xbmcgui.getCurrentWindowDialogId()) - # init the monitor class to catch onscreensaverdeactivated calls - self.Monitor = MyMonitor(action = self._exit) - self.stop = False - self.startup = True - self.offset = 0 - - def _get_settings(self): - # read addon settings - self.slideshow_type = ADDON.getSettingInt('type') - self.slideshow_path = ADDON.getSettingString('path') - self.slideshow_effect = ADDON.getSettingInt('effect') - # labelenum is broken, we use enum and get the index (index 0 = 2 secs) - self.slideshow_time = ADDON.getSettingInt('time') + 2 - # convert float to hex value usable by the skin - self.slideshow_dim = hex(int('%.0f' % (float(100 - ADDON.getSettingInt('level')) * 2.55)))[2:] + 'ffffff' - self.slideshow_overlay = ADDON.getSettingBool('overlay') - self.slideshow_recursive = ADDON.getSettingBool('recursive') - self.slideshow_random = ADDON.getSettingBool('random') - self.slideshow_resume = ADDON.getSettingBool('resume') - self.slideshow_scale = ADDON.getSettingBool('scale') - self.slideshow_name = ADDON.getSettingInt('label') - self.slideshow_date = ADDON.getSettingBool('date') - self.slideshow_iptc = ADDON.getSettingBool('iptc') - self.slideshow_music = ADDON.getSettingBool('music') - self.slideshow_bg = ADDON.getSettingBool('background') - # select which image controls from the xml we are going to use - if self.slideshow_scale: - self.image1 = self.getControl(3) - self.image2 = self.getControl(4) - self.getControl(1).setVisible(False) - self.getControl(2).setVisible(False) - self.getControl(5).setVisible(False) - self.getControl(6).setVisible(False) - else: - self.image1 = self.getControl(1) - self.image2 = self.getControl(2) - self.getControl(3).setVisible(False) - self.getControl(4).setVisible(False) - if self.slideshow_bg: - self.image3 = self.getControl(5) - self.image4 = self.getControl(6) - if self.slideshow_name == 0: - self.getControl(99).setVisible(False) - else: - self.namelabel = self.getControl(99) - self.datelabel = self.getControl(100) - self.textbox = self.getControl(101) - # set the dim property - self._set_prop('Dim', self.slideshow_dim) - # show vignette overlay during slideshow if enabled - if self.slideshow_overlay: - self._set_prop('Overlay', 'show') - # show music info during slideshow if enabled - if self.slideshow_music: - self._set_prop('Music', 'show') - # show background if enabled - if self.slideshow_bg: - self._set_prop('Background', 'show') - - def _start_show(self, items): - # we need to start the update thread after the deep copy of self.items finishes - thread = img_update(data=self._get_items) - thread.start() - # start with image 1 - cur_img = self.image1 - order = [1,2] - # loop until onScreensaverDeactivated is called - while (not self.Monitor.abortRequested()) and (not self.stop): - # keep track of image position, needed to save the offset - self.position = self.offset - # iterate through all the images - for img in items[self.offset:]: - # cache file may be outdated - if self.slideshow_type == 2 and not xbmcvfs.exists(img[0]): - continue - # add image to gui - useCache = False - if img[0].startswith('http'): # do not redownload images from online sources each time - useCache = True - cur_img.setImage(img[0],useCache) - # add background image to gui - if (not self.slideshow_scale) and self.slideshow_bg: - if order[0] == 1: - self.image3.setImage(img[0],useCache) - else: - self.image4.setImage(img[0],useCache) - # give xbmc some time to load the image - if not self.startup: - xbmc.sleep(1000) - else: - self.startup = False - # get exif and iptc tags if enabled in settings and we have an image that can contain this data - datetime = '' - title = '' - description = '' - keywords = '' - exif = False - iptc_ti = False - iptc_de = False - iptc_ke = False - if self.slideshow_type == 2 and (self.slideshow_date or self.slideshow_iptc) and (os.path.splitext(img[0])[1].lower() in EXIF_TYPES): - # get exif date - if self.slideshow_date: - exiffile = BinaryFile(img[0]) - try: - exiftags = exifread.process_file(exiffile, details=False, stop_tag='DateTimeOriginal') - if 'EXIF DateTimeOriginal' in exiftags: - datetime = exiftags['EXIF DateTimeOriginal'].values - # sometimes exif date returns useless data, probably no date set on camera - if datetime == '0000:00:00 00:00:00': - datetime = '' - else: - try: - # localize the date format - date = datetime[:10].split(':') - time = datetime[10:] - if DATEFORMAT[1] == 'm': - datetime = date[1] + '-' + date[2] + '-' + date[0] + ' ' + time - elif DATEFORMAT[1] == 'd': - datetime = date[2] + '-' + date[1] + '-' + date[0] + ' ' + time - else: - datetime = date[0] + '-' + date[1] + '-' + date[2] + ' ' + time - except: - pass - exif = True - except: - pass - exiffile.close() - # get iptc title, description and keywords - if self.slideshow_iptc: - iptcfile = BinaryFile(img[0]) - try: - iptc = IPTCInfo(iptcfile) - if iptc['object name']: - title = bytes(iptc['object name']).decode('utf-8') - iptc_ti = True - if iptc['caption/abstract']: - description = bytes(iptc['caption/abstract']).decode('utf-8') - iptc_de = True - if iptc['keywords']: - tags = [] - for tag in iptc['keywords']: - tags.append(bytes(tag).decode('utf-8')) - keywords = ', '.join(tags) - iptc_ke = True - except: - pass - iptcfile.close() - # get xmp title, description and subject - if (not iptc_ti or not iptc_de or not iptc_ke): - try: - xmpfile = xbmcvfs.File(img[0]) - data = xmpfile.readBytes().decode('cp437') - xmpdata = re.search(r'(.*?)(.*?)<', xmpdata.group(1), flags=re.DOTALL) - if titlematch and not iptc_ti: - title = titlematch.group(1).encode('cp437').decode('utf-8') - iptc_ti = True - descmatch = re.search(r'(.*?)<', xmpdata.group(1), flags=re.DOTALL) - if descmatch and not iptc_de: - description = descmatch.group(1).encode('cp437').decode('utf-8') - iptc_de = True - subjmatch = re.search(r'(.*?)', '').replace('', '') - iptc_ke = True - except: - pass - xmpfile.close() - # display exif date if we have one - if exif: - self.datelabel.setLabel('[I]' + datetime + '[/I]') - self.datelabel.setVisible(True) - else: - self.datelabel.setVisible(False) - # display iptc data if we have any - if iptc_ti or iptc_de or iptc_ke: - self.textbox.setText('[CR]'.join([title, keywords] if title == description else [title, description, keywords])) - self.textbox.setVisible(True) - else: - self.textbox.setVisible(False) - # get the file or foldername if enabled in settings - if self.slideshow_name != 0: - if self.slideshow_name == 1: # filename - if self.slideshow_type == 2: - if self.slideshow_path.startswith('plugin://'): - NAME, EXT = os.path.splitext(os.path.basename(img[1])) - else: - NAME, EXT = os.path.splitext(os.path.basename(img[0])) - else: - NAME = img[1] - elif self.slideshow_name == 2: # directory name - if self.slideshow_path.startswith('plugin://'): - NAME, EXT = os.path.splitext(os.path.basename(img[1])) # only filename is available - else: - ROOT, NAME = os.path.split(os.path.dirname(img[0])) - elif self.slideshow_name == 3: # directory name / filename - if self.slideshow_type == 2: - if self.slideshow_path.startswith('plugin://'): - NAME, EXT = os.path.splitext(os.path.basename(img[1])) # only filename is available - else: - ROOT, FOLDER = os.path.split(os.path.dirname(img[0])) - FILE, EXT = os.path.splitext(os.path.basename(img[0])) - NAME = FOLDER + ' / ' + FILE - else: - ROOT, FOLDER = os.path.split(os.path.dirname(img[0])) - NAME = FOLDER + ' / ' + img[1] - elif self.slideshow_name == 4: # full path - if self.slideshow_path.startswith('plugin://'): - NAME, EXT = os.path.splitext(os.path.basename(img[1])) # only filename is available - else: - NAME = os.path.realpath(img[0]) - self.namelabel.setLabel(NAME) - # set animations - if self.slideshow_effect == 0: - # add slide anim - self._set_prop('Slide%d' % order[0], '0') - self._set_prop('Slide%d' % order[1], '1') - elif self.slideshow_effect == 1 or self.slideshow_effect == 2: - # add random slide/zoom anim - if self.slideshow_effect == 2: - # add random slide/zoom anim - self._anim(cur_img) - # add fade anim, used for both fade and slide/zoom anim - self._set_prop('Fade%d' % order[0], '0') - self._set_prop('Fade%d' % order[1], '1') - elif self.slideshow_effect == 3: - # we need to hide the images when no effect is selected, add fade effect with time=0 - self._set_prop('NoEffectFade%d' % order[0], '0') - self._set_prop('NoEffectFade%d' % order[1], '1') - # add fade anim to background images - if self.slideshow_bg and self.slideshow_effect != 3: - self._set_prop('Fade1%d' % order[0], '0') - self._set_prop('Fade1%d' % order[1], '1') - # define next image - if cur_img == self.image1: - cur_img = self.image2 - order = [2,1] - else: - cur_img = self.image1 - order = [1,2] - # slideshow time in secs (we already slept for 1 second) - count = self.slideshow_time - 1 - # display the image for the specified amount of time - while (not self.Monitor.abortRequested()) and (not self.stop) and count > 0: - count -= 1 - xbmc.sleep(1000) - # break out of the for loop if onScreensaverDeactivated is called - if self.stop or self.Monitor.abortRequested(): - break - self.position += 1 - self.offset = 0 - items = copy.deepcopy(self.items) - - def _get_items(self, update=False): - self.slideshow_type = ADDON.getSettingInt('type') - log('slideshow type: %i' % self.slideshow_type) - # check if we have an image folder, else fallback to video fanart - if self.slideshow_type == 2: - hexfile = checksum(self.slideshow_path.encode('utf-8')) + '_' + str(self.slideshow_recursive) # check if path has changed, so we can create a new cache at startup - log('image path: %s' % self.slideshow_path) - log('update: %s' % update) - if (not xbmcvfs.exists(CACHEFILE % hexfile)) or update: # create a new cache if no cache exits or during the background scan - log('create cache') - create_cache(self.slideshow_path, hexfile) - self.items = self._read_cache(hexfile) - log('items: %s' % len(self.items)) - if not self.items: - self.slideshow_type = 0 - # delete empty cache file - if xbmcvfs.exists(CACHEFILE % hexfile): - xbmcvfs.delete(CACHEFILE % hexfile) - # video fanart - if self.slideshow_type == 0: - methods = [('VideoLibrary.GetMovies', 'movies'), ('VideoLibrary.GetTVShows', 'tvshows')] - # music fanart - elif self.slideshow_type == 1: - methods = [('AudioLibrary.GetArtists', 'artists')] - # query the db - if not self.slideshow_type == 2: - self.items = [] - for method in methods: - json_query = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "' + method[0] + '", "params": {"properties": ["art"]}, "id": 1}') - json_response = json.loads(json_query) - if 'result' in json_response and json_response['result'] != None and method[1] in json_response['result']: - for item in json_response['result'][method[1]]: - if 'fanart' in item['art']: - self.items.append([item['art']['fanart'], item['label']]) - # randomize - if self.slideshow_random: - random.seed() - random.shuffle(self.items) - - def _get_offset(self): - try: - offset = xbmcvfs.File(RESUMEFILE) - self.offset = int(offset.read()) - offset.close() - except: - self.offset = 0 - - def _save_offset(self): - if not xbmcvfs.exists(CACHEFOLDER): - xbmcvfs.mkdir(CACHEFOLDER) - try: - offset = xbmcvfs.File(RESUMEFILE, 'w') - offset.write(str(self.position)) - offset.close() - except: - log('failed to save resume point') - - def _read_cache(self, hexfile): - try: - cache = xbmcvfs.File(CACHEFILE % hexfile) - images = json.load(cache) - cache.close() - except: - images = [] - return images - - def _anim(self, cur_img): - # reset position the current image - cur_img.setPosition(0, 0) - # pick a random anim - number = random.randint(0,8) - posx = 0 - posy = 0 - # add 1 sec fadeout time to showtime - anim_time = self.slideshow_time + 1 - # set zoom level depending on the anim time - zoom = 110 + anim_time - if number == 1 or number == 5 or number == 7: - posx = int(-1920 + (19.2 * anim_time) + 0.5) - elif number == 2 or number == 6 or number == 8: - posx = int(1920 - (19.2 * anim_time) + 0.5) - if number == 3 or number == 5 or number == 6: - posy = int(-1080 + (10.8 * anim_time) + 0.5) - elif number == 4 or number == 7 or number == 8: - posy = int(1080 - (10.8 * anim_time) + 0.5) - # position the current image - cur_img.setPosition(posx, posy) - # add the animation to the current image - if number == 0: - cur_img.setAnimations(eval(EFFECTLIST[number] % (self.adj_time))) - else: - cur_img.setAnimations(eval(EFFECTLIST[number] % (self.adj_time, zoom, zoom, self.adj_time))) - - def _get_animspeed(self): - # find the skindir - json_query = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Addons.GetAddonDetails", "params": {"addonid": "%s", "properties": ["path"]}, "id": 1}' % SKINDIR) - json_response = json.loads(json_query) - if 'result' in json_response and (json_response['result'] != None) and 'addon' in json_response['result'] and 'path' in json_response['result']['addon']: - skinpath = json_response['result']['addon']['path'] - else: - log('failed to retrieve skin path') - log(SKINDIR) - log(json_query) - return - skinxml = xbmcvfs.translatePath(os.path.join(skinpath, 'addon.xml')) - try: - # parse the skin addon.xml - self.xml = parse(skinxml) - # find all extension tags - tags = self.xml.documentElement.getElementsByTagName( 'extension' ) - for tag in tags: - # find the effectslowdown attribute - for (name, value) in tag.attributes.items(): - if name == 'effectslowdown': - anim = value - return anim - except: - log('failed to parse addon.xml') - return - - def _set_prop(self, name, value): - self.winid.setProperty('SlideView.%s' % name, value) - - def _clear_prop(self, name): - self.winid.clearProperty('SlideView.%s' % name) - - def _exit(self): - # exit when onScreensaverDeactivated gets called - self.stop = True - # clear our properties on exit - self._clear_prop('Slide1') - self._clear_prop('Slide2') - self._clear_prop('Fade1') - self._clear_prop('Fade2') - self._clear_prop('Fade11') - self._clear_prop('Fade12') - self._clear_prop('Dim') - self._clear_prop('Overlay') - self._clear_prop('Music') - self._clear_prop('Splash') - self._clear_prop('Background') - # save the current position to file - if self.slideshow_type == 2 and not self.slideshow_random and self.slideshow_resume: - self._save_offset() - self.close() - - -class img_update(threading.Thread): - def __init__( self, *args, **kwargs ): - self._get_items = kwargs['data'] - threading.Thread.__init__(self) - self.stop = False - self.Monitor = MyMonitor(action = self._exit) - - def run(self): - while (not self.Monitor.abortRequested()) and (not self.stop): - # create a fresh index as quickly as possible after slidshow started - self._get_items(True) - count = 0 - while count != 3600: # check for new images every hour - xbmc.sleep(1000) - count += 1 - if self.Monitor.abortRequested() or self.stop: - return - - def _exit(self): - # exit when onScreensaverDeactivated gets called - self.stop = True - - -class MyMonitor(xbmc.Monitor): - def __init__( self, *args, **kwargs ): - self.action = kwargs['action'] - - def onScreensaverDeactivated(self): - self.action() - - def onDPMSActivated(self): - self.action() diff --git a/screensaver.picture.slideshow/lib/utils.py b/screensaver.picture.slideshow/lib/utils.py deleted file mode 100644 index 30a84a6cb..000000000 --- a/screensaver.picture.slideshow/lib/utils.py +++ /dev/null @@ -1,145 +0,0 @@ -import hashlib -import os -import json -import re -import sys -import urllib -import xbmc -import xbmcvfs -import xbmcaddon -import xml.etree.ElementTree as etree - -ADDON = xbmcaddon.Addon() -ADDONID = ADDON.getAddonInfo('id') -LANGUAGE = ADDON.getLocalizedString - -# supported image types by the screensaver -IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.tif', '.tiff', '.gif', '.pcx', '.bmp', '.tga', '.ico', '.nef', '.webp', '.jp2', '.apng'] -HEIF_TYPES = ['.heic', '.heif'] -MPO_TYPES = ['.mpo'] -RAW_TYPES = ['.3fr', '.arw', '.cr2', '.crw', '.dcr', '.dng', '.erf', '.kdc', '.mdc', '.mef', '.mos', '.mrw', '.nef', '.nrw', '.orf', '.pef', '.ppm', '.raf', '.raw', '.rw2', '.srw', '.x3f'] -CACHEFOLDER = xbmcvfs.translatePath(ADDON.getAddonInfo('profile')) -CACHEFILE = os.path.join(CACHEFOLDER, 'cache_%s') -RESUMEFILE = os.path.join(CACHEFOLDER, 'offset') -ASFILE = xbmcvfs.translatePath('special://profile/advancedsettings.xml') - -def log(txt): - message = '%s: %s' % (ADDONID, txt) - xbmc.log(msg=message, level=xbmc.LOGDEBUG) - -def checksum(path): - return hashlib.md5(path).hexdigest() - -def create_cache(path, hexfile): - images = walk(path) - if not xbmcvfs.exists(CACHEFOLDER): - xbmcvfs.mkdir(CACHEFOLDER) - # remove old cache files - dirs, files = xbmcvfs.listdir(CACHEFOLDER) - for item in files: - if item != 'settings.xml': - xbmcvfs.delete(os.path.join(CACHEFOLDER,item)) - if images: - # create cache file - try: - cache = xbmcvfs.File(CACHEFILE % hexfile, 'w') - json.dump(images, cache) - cache.close() - except: - log('failed to save cachefile') - -def get_excludes(): - regexes = [] - if xbmcvfs.exists(ASFILE): - try: - tree = etree.parse(ASFILE) - root = tree.getroot() - excludes = root.find('pictureexcludes') - if excludes is not None: - for expr in excludes: - regexes.append(expr.text) - except: - pass - return regexes - -def walk(path): - images = [] - folders = [] - excludes = get_excludes() - # multipath support - if path.startswith('multipath://'): - # get all paths from the multipath - paths = path[12:-1].split('/') - for item in paths: - folders.append(urllib.unquote_plus(item)) - else: - folders.append(path) - for folder in folders: - if xbmcvfs.exists(xbmcvfs.translatePath(folder)): - dirs = [] - files = [] - if xbmc.getCondVisibility('System.HasAddon(imagedecoder.heif)'): - IMAGE_TYPES.extend(HEIF_TYPES) - if xbmc.getCondVisibility('System.HasAddon(imagedecoder.mpo)'): - IMAGE_TYPES.extend(MPO_TYPES) - if xbmc.getCondVisibility('System.HasAddon(imagedecoder.raw)'): - IMAGE_TYPES.extend(RAW_TYPES) - # get all files and subfolders - if folder.startswith('plugin://'): - getroot = xbmc.executeJSONRPC('{"jsonrpc":"2.0", "method":"Files.GetDirectory", "params":{"directory":"%s", "sort":{"method":"label"}}, "id":1 }' % folder) - root = json.loads(getroot) - if 'result' in root and 'files' in root["result"]: - for item in root["result"]["files"]: - if item["filetype"] == "file": - files.append(item) - elif item["filetype"] == "directory": - dirs.append(item["file"]) - else: - dirs, files = xbmcvfs.listdir(folder) - log('dirs: %s' % len(dirs)) - log('files: %s' % len(files)) - if not folder.startswith('plugin://'): - # natural sort - convert = lambda text: int(text) if text.isdigit() else text - alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] - files.sort(key=alphanum_key) - for item in files: - # check pictureexcludes from as.xml - fileskip = False - if excludes: - for string in excludes: - regex = re.compile(string) - if folder.startswith('plugin://'): - match = regex.search(item["label"]) - else: - match = regex.search(item) - if match: - fileskip = True - break - # filter out all images - if folder.startswith('plugin://'): - if os.path.splitext(item["label"])[1].lower() in IMAGE_TYPES and not fileskip: - images.append([item["file"], item["label"]]) - else: - if os.path.splitext(item)[1].lower() in IMAGE_TYPES and not fileskip: - images.append([os.path.join(folder,item), item]) - if xbmcaddon.Addon().getSettingBool('recursive'): - for item in dirs: - # check pictureexcludes from as.xml - dirskip = False - if excludes: - for string in excludes: - regex = re.compile(string) - match = regex.search(item) - if match: - dirskip = True - break - # recursively scan all subfolders - if not dirskip: - if item.startswith('plugin://'): - images += walk(item) - else: - images += walk(os.path.join(folder,item,'')) # make sure paths end with a slash - else: - log('folder does not exist') - return images diff --git a/screensaver.picture.slideshow/resources/fanart.jpg b/screensaver.picture.slideshow/resources/fanart.jpg deleted file mode 100644 index bb4461b58..000000000 Binary files a/screensaver.picture.slideshow/resources/fanart.jpg and /dev/null differ diff --git a/screensaver.picture.slideshow/resources/icon.png b/screensaver.picture.slideshow/resources/icon.png deleted file mode 100644 index e8715b8bc..000000000 Binary files a/screensaver.picture.slideshow/resources/icon.png and /dev/null differ diff --git a/screensaver.picture.slideshow/resources/language/resource.language.af_za/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.af_za/strings.po deleted file mode 100644 index 6309e8ea1..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.af_za/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Afrikaans (http://www.transifex.com/projects/p/xbmc-addons/language/af/)\n" -"Language: af\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Sluimerskerm wat prente vertoon as skyfievertoning" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Die Skyfievertoning sluimerskerm wys jou 'n skyfie vertoning met verskeie oorgang effekte. Dit kan gestel word om jou biblioteek se musiek of video aanhangerkuns, of 'n persoonlike lêer met prente te vertoon." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Bron van skyfievertoning prente" - -msgctxt "#30001" -msgid "Folder" -msgstr "Vouer" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Aanhangerkuns" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Musiek Aanhangerkuns" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Prente Lêer" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Verdof vlak" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effek" - -msgctxt "#30007" -msgid "Slide" -msgstr "Skyfie" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Kruisdemp" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Beeldrol en zoem" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Hoeveelheid sekondes om elke beeld te vertoon" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Skaleer beelde volskerm" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Vertoon beeld naam" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Nie in staat gestel" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Gebruik lêer naam" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Gebruik vouer naam" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Vertoon beelde in lukraak volgorde" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Vertoon prent datum" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Vertoon addisionele prent etikette" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Vertoon musiek inligting gedurende oudio terugspeel" - -msgctxt "#30024" -msgid "Basic" -msgstr "Basies" - -msgctxt "#30025" -msgid "Additional" -msgstr "Addisioneel" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.am_et/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.am_et/strings.po deleted file mode 100644 index 9317da1c4..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.am_et/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-07-22 14:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Amharic (Ethiopia) \n" -"Language: am_et\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "መመልከቻ ማዳኛ የ ምስል ተንሸራታች ማሳያ" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "የ ተንሸራታች ማሳያ መመልከቻ ማዳኛ የተለያዩ ተንሸራታች ማሳያ ምስሎች በ ተለያዩ መሸጋገሪያ ውጤቶችን በመጠቀም ያሳይዎታል፡ ማዋቀር ይችላሉ የ ሙዚቃ መጻህፍት ቤት ወይንም የ ቪዲዮ ወይንም የምስሎች ፎልደር እንዲያሳይ" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "የ ተንሸራታች ማሳያ ምስሎች ምንጭ" - -msgctxt "#30001" -msgid "Folder" -msgstr "ፎልደር" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "የ ቪዲዮ ደጋፊዎች ስእል" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "የ ሙዚቃ ደጋፊዎች ስእል" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "የምስል ፎልደር" - -msgctxt "#30005" -msgid "Dim level" -msgstr "የማደብዘዣ ደረጃ" - -msgctxt "#30006" -msgid "Effect" -msgstr "ውጤቱ" - -msgctxt "#30007" -msgid "Slide" -msgstr "ተንሸራታች" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "መስቀልኛ ማፍዘዥ" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "መፈለጊያ እና ማሳያ" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "እያንዳንዱ ምስል የሚታይበት ጊዜ በሰከንድ" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "በሙሉ መመልከቻ ዘዴ የ ምስሎች መመጠኛ" - -msgctxt "#30012" -msgid "Display image name" -msgstr "የ ምስሉን ስም ማሳያ" - -msgctxt "#30013" -msgid "Disabled" -msgstr "ተሰናክሏል" - -msgctxt "#30014" -msgid "Use filename" -msgstr "የ ፋይል ስም መጠቀሚያ" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "የ ፎልደር ስም መጠቀሚያ" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "ምስሎቹን በነሲብ ቅደም ተከተል ማሳያ" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "ፎልደር- እና የ ፋይል ስም ይጠቀሙ" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "የ ስእል ቀን ማሳያ" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "ተጨማሪ የ ስእል ቁራጭ ማሳያ" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "ድምፅ በሚጫወት ጊዜ የ ሙዚቃ መረጃ ማሳያ" - -msgctxt "#30024" -msgid "Basic" -msgstr "መሰረታዊ" - -msgctxt "#30025" -msgid "Additional" -msgstr "ተጨማሪ" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ar_sa/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ar_sa/strings.po deleted file mode 100644 index 68370d552..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ar_sa/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Arabic (http://www.transifex.com/projects/p/xbmc-addons/language/ar/)\n" -"Language: ar\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "شاشة التوقف هي التي تعرض صورة تلقائيا" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "شاشة توقف العرض التلقائي ستظهر لك عرض تدريجي للصور باستخدام تأثيرات تحول مختلفة. ممكن أن تكون كونت لتعرض مكتبة الموسيقى أو الفيديو الخاصة بك, أو مجلد مخصص من الصور." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "مصدر صور العرض التلقائي" - -msgctxt "#30001" -msgid "Folder" -msgstr "مجلد" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "فيديوFanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "موسيقىFanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "مجلد الصور" - -msgctxt "#30005" -msgid "Dim level" -msgstr "مستوى ضعيف" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "معطلة" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "أساسي" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.az_az/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.az_az/strings.po deleted file mode 100644 index b31e9d5eb..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.az_az/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: Kodi add-ons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: az_az\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.be_by/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.be_by/strings.po deleted file mode 100644 index f8df455d0..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.be_by/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-07-22 14:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Belarusian \n" -"Language: be_by\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Ахоўнік экрану што робіць паказ слайдаў" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Слайдавы ахоўнік экрану будзе паказваць выявы з дапамогай разнастайных пераходных эфектаў. Можна сканфігураваць для паказу вашых бібліятэк музыкі ці відэа, альбо пэўных каталогаў з выявамі" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Крыніца выяў для слаўдаў" - -msgctxt "#30001" -msgid "Folder" -msgstr "Тэчка" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Відэа Фанарт" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Музычны Фанарт" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Каталог Выяў" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Узровень Dim" - -msgctxt "#30006" -msgid "Effect" -msgstr "Эфект" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Перакрыццё" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Колькасць секунд на кожную выяву" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Расцягнуць выявы на ўвесь экран" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Паказваць імя выявы" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Забаронена" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Ужыць імя файла" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Ужыць імя тэчкі" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Паказваць выявы ў выпадковым парадку" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Паказваць дату выявы" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Паказваць дадатковыя тэгі выявы" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Паказваць звесткі музыкі падчас прайгравання" - -msgctxt "#30024" -msgid "Basic" -msgstr "Асноўны" - -msgctxt "#30025" -msgid "Additional" -msgstr "Дадатковы" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.bg_bg/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.bg_bg/strings.po deleted file mode 100644 index ed4d2551c..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.bg_bg/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Bulgarian (http://www.transifex.com/projects/p/xbmc-addons/language/bg/)\n" -"Language: bg\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Скрийнсейвър, изобразяващ презентация от картини" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Скрийнсейвърът „Презентация“ изобразява презентация от картини с разнообразни ефекти при прехода между отделните картини. Можете да зададете като източник на картини фен-арт изображенията от видео/музикалната библиотеката или персонална папка с изображения." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Източник на изображения" - -msgctxt "#30001" -msgid "Folder" -msgstr "Папка" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Фен-арт на видео файловете" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Фен-арт на аудио файловете" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Папка с изображения" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Степен на затъмняване" - -msgctxt "#30006" -msgid "Effect" -msgstr "Ефект" - -msgctxt "#30007" -msgid "Slide" -msgstr "Плъзгане" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Плавен преход" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Изобразяване всяко изображение за (Х сек)" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Показвай имената на снимките" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Изключена" - -msgctxt "#30014" -msgid "Use filename" -msgstr "..името на файла" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "...името на папката" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Показвай изображенията в произволен ред" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Ползвай папка и име на файла" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Показвай датата" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Основни" - -msgctxt "#30025" -msgid "Additional" -msgstr "Допълнителни" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.bs_ba/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.bs_ba/strings.po deleted file mode 100644 index 5335743e5..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.bs_ba/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Bosnian (http://www.transifex.com/projects/p/xbmc-addons/language/bs/)\n" -"Language: bs\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Fascikle" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Onemogućeno" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ca_es/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ca_es/strings.po deleted file mode 100644 index 889b83ddb..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ca_es/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Catalan (http://www.transifex.com/projects/p/xbmc-addons/language/ca/)\n" -"Language: ca\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Estalvi de pantalla que mostra una presentació de diapositives d'imatges" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "El protector de pantalles Projecció mostrarà una projecció de diapositives utilitzant diferents efectes de transició. Es pot configurar per mostrar el fanart de les vostres biblioteques de música o un directori d'imatges personalitzat" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Font de les imatges de la presentació" - -msgctxt "#30001" -msgid "Folder" -msgstr "Carpeta" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart de vídeo" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart de música" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Carpeta de les imatges" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nivell d'atenuació" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efecte" - -msgctxt "#30007" -msgid "Slide" -msgstr "Diapositiva" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Nombre de segons per mostrar cada imatge" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Escala de les imatges a pantalla completa" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Nom de l'imatge a mostrar per pantalla" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Inhabilitat" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Utilitza nom d'arxiu" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Utilitza el nom de la carpeta" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Mostrar les imatges en ordre aleatori" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Mostrar la data de la foto" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Mostra etiquetes d'imatge addicionals" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Mostra informació de música durant la reproducció d'àudio" - -msgctxt "#30024" -msgid "Basic" -msgstr "Bàsic" - -msgctxt "#30025" -msgid "Additional" -msgstr "Adicional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.cs_cz/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.cs_cz/strings.po deleted file mode 100644 index 984b1ec7a..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.cs_cz/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-06-22 08:43+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Czech \n" -"Language: cs_cz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 4.13\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Spořič obrazovky promítající obrázky" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Spořič obrazovky promítání vám bude promítat zvolené obrázky s použitím různých přechodových efektů. Může být nastaven k zobrazování fanart z vašich hudebních, či video knihoven, nebo vlastní složky obrázků" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Zdroj promítaných obrázků" - -msgctxt "#30001" -msgid "Folder" -msgstr "Složka" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Hudební Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Složka s obrázky" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Úroveň ztmavení" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Snímek" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Prolnutí" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Posun a přiblížení" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Doba zobrazení každého obrázku (sekundy)" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Škálovat obrázek na rozlišení celé obrazovky" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Zobrazit název obrázku" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Zakázáno" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Použít název souboru" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Použít název složky" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Zobrazit obrázky v náhodném pořadí" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Použij složku- a jméno souboru" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Zobrazit datum obrázku" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Zobrazit další značky obrázku" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Zobrazit informace o hudbě při přehrávání zvuku" - -msgctxt "#30024" -msgid "Basic" -msgstr "Základní" - -msgctxt "#30025" -msgid "Additional" -msgstr "Další" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Zobrazit obrázek na pozadí" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.cy_gb/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.cy_gb/strings.po deleted file mode 100644 index 6c5489b39..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.cy_gb/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Welsh (http://www.transifex.com/projects/p/xbmc-addons/language/cy/)\n" -"Language: cy\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Arbedwr sgrin sy'n dangos sioe o ddelweddau" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Bydd yr arbedwr sgrin Slideshow yn dangos sioe o ddelweddau dan ddefnyddio amryw o effeithiau trawsnewidiol.Mae modd ei ffurfweddu i ddangos celf eich cerddoriaeth neu fideo neu ffolder cyfaddas o ddelweddau." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Ffynhonnell delweddau sioe sleidiau" - -msgctxt "#30001" -msgid "Folder" -msgstr "Ffolder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Celf fideo" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Celf cerddoriaeth" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Ffolder Delweddau" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Lefel pylu" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effaith" - -msgctxt "#30007" -msgid "Slide" -msgstr "Sleid" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Croes bylu" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Tremio a chwyddo" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Sawl eiliadau i ddangos pob delwedd" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Graddio pob delwedd i'r sgrin lawn" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Dangos enw delwedd" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Analluogwyd" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Defnyddio enw ffeil" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Defnyddio enw ffolder" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Dangos delweddau ar hap" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Dangos dyddiad llun" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Dangos tagiau lluniau ychwanegol" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Dangos gwybodaeth cerddoriaeth wrth chwarae sain" - -msgctxt "#30024" -msgid "Basic" -msgstr "Elfennol" - -msgctxt "#30025" -msgid "Additional" -msgstr "Ychwanegol" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.da_dk/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.da_dk/strings.po deleted file mode 100644 index 50aedd94d..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.da_dk/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-07-13 13:14+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Danish \n" -"Language: da_dk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.13\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Pauseskærm som afspiller et diasshow" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Diasshow-pauseskærmen vil vise et diasshow af billeder ved hjælp af forskellige overgangseffekter. Den kan konfigureres til at vise fankunst fra dit biblioteks musik eller videoer, eller billeder fra en selvvalgt mappe." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Kilde til billeder i diasshow" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mappe" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video-fankunst" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Musik-fankunst" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Billedemappe" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Dæmpningsniveau" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Dias" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Glidende overgang" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panorering og zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Antal sekunder hvert billede bliver vist" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Skaler billeder til fuld skærm" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Vis billednavn" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Deaktiveret" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Anvend filnavn" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Anvend mappenavn" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Vis billederne i tilfældig rækkefølge" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Brug fuld sti" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Brug mappe- og filnavn" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Vis billeddato" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Vis yderligere billedetiketter" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Vis information om musikken mens den bliver afspillet" - -msgctxt "#30024" -msgid "Basic" -msgstr "Simpel" - -msgctxt "#30025" -msgid "Additional" -msgstr "Yderligere" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Genoptag diasshow fra sidste position" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Vis baggrundsbillede" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Brug vignette øverst" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.de_de/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.de_de/strings.po deleted file mode 100644 index b496bda9c..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.de_de/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2023-01-31 22:15+0000\n" -"Last-Translator: Demian \n" -"Language-Team: German \n" -"Language: de_de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.15.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Bildschirmschoner, der eine Bilder-Diashow anzeigt" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Der Slideshow-Bildschirmschoner zeigt eine Bilder-Diashow mit verschiedenen Übergangseffekten. Er kann so konfiguriert werden, dass Musik- oder Videofanart aus der Bibliothek oder Bilder eines bestimmten Ordners angezeigt werden" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Quelle der Diashowbilder" - -msgctxt "#30001" -msgid "Folder" -msgstr "Ordner" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Videofanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Musikfanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Bildordner" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Abdunkelungsgrad" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Seite" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Überblendung" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Schwenk und Zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Anzahl an Sekunden, die jedes Bild angezeigt wird" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Bilder auf Vollbild skalieren" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Bildname anzeigen" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Deaktiviert" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Dateiname verwenden" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Ordnername verwenden" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Bilder in zufälliger Reihenfolge anzeigen" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Vollständigen Pfad verwenden" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Ordner- und Dateiname verwenden" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Bilddatum anzeigen" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Zusätzliche Bildschlagwörter anzeigen" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Musikinformationen während der Wiedergabe anzeigen" - -msgctxt "#30024" -msgid "Basic" -msgstr "Basis" - -msgctxt "#30025" -msgid "Additional" -msgstr "Zusätzlich" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Diashow von letzter Position vorsetzen" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Hintergrundbild anzeigen" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Vignettenoverlay verwenden" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.el_gr/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.el_gr/strings.po deleted file mode 100644 index 7e6b8fbbe..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.el_gr/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Greek (http://www.transifex.com/projects/p/xbmc-addons/language/el/)\n" -"Language: el\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Μία προφύλαξη οθόνης παρουσίασης διαφανειών" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Η προφύλαξη οθόνης Slideshow θα εμφανίσει μία σειρά εικόνων χρησιμοποιώντας ένα πλήθος εφέ μετάβασης. Μπορεί να ρυθμιστεί ούτως ώστε να εμφανίζει τις εικόνες fanart της μουσικής ή βίντεο συλλογής σας, ή έναν προσωπικό φάκελο εικόνων" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Πηγή εικόνων παρουσίασης διαφανειών" - -msgctxt "#30001" -msgid "Folder" -msgstr "Φάκελος" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart Βίντεο" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart Μουσικής" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Φάκελος Εικόνων" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Επίπεδο αμαύρωσης" - -msgctxt "#30006" -msgid "Effect" -msgstr "Εφέ" - -msgctxt "#30007" -msgid "Slide" -msgstr "Διαφάνεια" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Σταδιακό σβήσιμο" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Εφέ pan & zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Δευτερόλεπτα προβολής κάθε εικόνας" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Εικόνες σε κλίμακα στην πλήρη οθόνη" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Προβολή ονόματος εικόνας" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Ανενεργή" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Χρήση ονόματος αρχείου" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Χρήση ονόματος φακέλου" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Εμφάνιση εικόνων σε τυχαία σειρά" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Χρήση φακέλου- και ονόματος αρχείου" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Προβολή της ημερομηνίας της εικόνας" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Προβολή επιπλέον ετικετών της εικόνας" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Προβολή μουσικών πληροφοριών κατά την αναπαραγωγή" - -msgctxt "#30024" -msgid "Basic" -msgstr "Βασικό" - -msgctxt "#30025" -msgid "Additional" -msgstr "Επιπρόσθετα" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Προβολή εικόνας παρασκηνίου" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.en_au/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.en_au/strings.po deleted file mode 100644 index 9648aee3d..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.en_au/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: English (Australia) (http://www.transifex.com/projects/p/xbmc-addons/language/en_AU/)\n" -"Language: en_AU\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Folder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Image Folder" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Dim level" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Disabled" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Basic" - -msgctxt "#30025" -msgid "Additional" -msgstr "Additional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.en_gb/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index ba0adb0b5..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: Kodi add-ons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en_gb\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 - -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 - -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.en_nz/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.en_nz/strings.po deleted file mode 100644 index f3e14e090..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.en_nz/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: English (New Zealand) (http://www.transifex.com/projects/p/xbmc-addons/language/en_NZ/)\n" -"Language: en_NZ\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Screensaver that displays an image slideshow" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Source of slideshow images" - -msgctxt "#30001" -msgid "Folder" -msgstr "Folder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Music Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Image Folder" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Dim level" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effect" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Crossfade" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Pan and zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Amount of seconds to display each image" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Scale images fullscreen" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Display image name" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Disabled" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Use filename" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Use foldername" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Display images in random order" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Use folder- and filename" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Display picture date" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Display additional picture tags" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Display music info during audio playback" - -msgctxt "#30024" -msgid "Basic" -msgstr "Basic" - -msgctxt "#30025" -msgid "Additional" -msgstr "Additional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Display background picture" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.en_us/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.en_us/strings.po deleted file mode 100644 index 7773b0da1..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.en_us/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: English (US) (http://www.transifex.com/projects/p/xbmc-addons/language/en_US/)\n" -"Language: en_US\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Screensaver that displays an image slideshow" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Source of slideshow images" - -msgctxt "#30001" -msgid "Folder" -msgstr "Folder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Music Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Image Folder" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Dim level" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effect" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Crossfade" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Pan and zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Amount of seconds to display each image" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Scale images fullscreen" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Display image name" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Disabled" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Use filename" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Use foldername" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Display images in random order" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Use folder- and filename" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Display picture date" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Display additional picture tags" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Display music info during audio playback" - -msgctxt "#30024" -msgid "Basic" -msgstr "Basic" - -msgctxt "#30025" -msgid "Additional" -msgstr "Additional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Display background picture" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Use vignette overlay" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.eo/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.eo/strings.po deleted file mode 100644 index 61659543e..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.eo/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-07-22 14:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Esperanto \n" -"Language: eo\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Malŝaltita" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.es_ar/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.es_ar/strings.po deleted file mode 100644 index a18a8dad7..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.es_ar/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/xbmc-addons/language/es_AR/)\n" -"Language: es_AR\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Un protector de pantalla que muestra una presentación de imágenes" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "El protector de pantalla Slideshow mostrará una presentación de imágenes usando varios efectos de transición. Puede ser configurado para mostrar los fanart de tus bibliotecas de música o video, o las imágenes de una carpeta." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Origen de las imágenes" - -msgctxt "#30001" -msgid "Folder" -msgstr "Carpeta" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart de video" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart de música" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Imagenes de una carpeta" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nivel de atenuación" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efecto" - -msgctxt "#30007" -msgid "Slide" -msgstr "Diapositiva" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Crossfade" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Desplazamiento y zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Número de segundos a mostrar cada imagen" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Escalar las imágenes en pantalla completa" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Mostrar nombre de la imágen" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Deshabilitado" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Usar nombre de archivo" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Usar nombre de carpeta" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Mostrar las imágenes en orden aleatorio" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Mostrar fecha de la imagen" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Mostrar etiquetas adicionales de la imagen" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Mostrar info de música durante reproducción de audio" - -msgctxt "#30024" -msgid "Basic" -msgstr "Básico" - -msgctxt "#30025" -msgid "Additional" -msgstr "Adicional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.es_es/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.es_es/strings.po deleted file mode 100644 index 9429a1b39..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.es_es/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2023-06-20 15:42+0000\n" -"Last-Translator: José Antonio Alvarado \n" -"Language-Team: Spanish (Spain) \n" -"Language: es_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.18\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Salvapantallas que muestra una presentación de imágenes" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "El salvapantallas Slideshow mostrará una presentación de imágenes usando varios efectos de transición. Puede ser configurado para mostrar los fanart de tus colecciones de música o video, o las imágenes de una carpeta" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Fuente de la presentación de imágenes" - -msgctxt "#30001" -msgid "Folder" -msgstr "Carpeta" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart de video" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart de música" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Carpeta de imágenes" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nivel de atenuación" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efecto" - -msgctxt "#30007" -msgid "Slide" -msgstr "Diapositiva" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Fundido cruzado" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Desplazamiento y zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Número de segundos a mostrar cada imagen" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Escalar las imágenes en pantalla completa" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Mostrar nombre de la imágen" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Desactivado" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Usar nombre de archivo" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Usar nombre de carpeta" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Mostrar las imágenes en orden aleatorio" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Usar ruta completa" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Usar nombre de carpeta y archivo" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Mostrar fecha de imagen" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Mostrar etiqueta adicional de imagen" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Mostrar información de la música durante la reproducción de audio" - -msgctxt "#30024" -msgid "Basic" -msgstr "Básico" - -msgctxt "#30025" -msgid "Additional" -msgstr "Adicional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Reanudar la presentación de imágenes desde la última posición" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Mostrar imágen de fondo" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Usar superposición de viñeta" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.es_mx/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.es_mx/strings.po deleted file mode 100644 index 2846869d8..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.es_mx/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Spanish (Mexico) (http://www.transifex.com/projects/p/xbmc-addons/language/es_MX/)\n" -"Language: es_MX\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Protector de pantalla que muestra una presentación de imágenes" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "El protector de pantalla Presentación de Imágenes te mostrará una presentación de imágenes usando varios efectos de transición. Puede ser configurado para mostrar tus librerías de fanart de música o video, o una carpeta de imágenes personalizada." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Fuente de imágenes de presentación" - -msgctxt "#30001" -msgid "Folder" -msgstr "Carpeta" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart de Video" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart de Música" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Carpeta de Imágenes" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nivel de atenuamiento" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Deshabilitado" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Básico" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.et_ee/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.et_ee/strings.po deleted file mode 100644 index 71069dd31..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.et_ee/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-08-22 10:14+0000\n" -"Last-Translator: rimasx \n" -"Language-Team: Estonian \n" -"Language: et_ee\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.13\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Ekraanisäästja, mis näitab piltide slaidiesitlust" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Slaidiesitluse ekraanisäästja näitab sulle slaidiesitlust kasutades erinevaid üleminekuefekte. Slaidiesitlust on võimalik koostada nii muusika või videote fännipiltidest kui ka kasutades selleks spetsiaalset määratud piltide kausta" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Slaidiesitluse piltide allikas" - -msgctxt "#30001" -msgid "Folder" -msgstr "Kaustanimi" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video fännipildid" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Muusika fännipildid" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Piltide kaust" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Hämarduse aste" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slaid" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Siire" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panoraamina ja suurenda" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Sekundite arv iga pildi kuvamiseks" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Suurenda pilt täisekraanile" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Näita pildi nime" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Ei kasutata" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Kasuta faili nime" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Kasuta kausta nime" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Näita pilte suvalises järjekorras" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Kasuta kausta- ja failinime" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Näita pildi kuupäeva" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Näita pildi täiendavaid silte" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Heli taasesitlusel näita muusika infot" - -msgctxt "#30024" -msgid "Basic" -msgstr "Põhiline" - -msgctxt "#30025" -msgid "Additional" -msgstr "Täiendav" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Jätka slaidiseanssi poolelijäänud kohast" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Kuva taustapilt" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.eu_es/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.eu_es/strings.po deleted file mode 100644 index 7ca9971f5..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.eu_es/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Basque (http://www.transifex.com/projects/p/xbmc-addons/language/eu/)\n" -"Language: eu\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Karpeta" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Dim maila" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Ezgaituta" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.fa_af/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.fa_af/strings.po deleted file mode 100644 index c62ceecb5..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.fa_af/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Persian (http://www.transifex.com/projects/p/xbmc-addons/language/fa/)\n" -"Language: fa\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "پوشه" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "غیرفعال" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.fa_ir/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.fa_ir/strings.po deleted file mode 100644 index 57d137dd0..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.fa_ir/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Persian (Iran) (http://www.transifex.com/projects/p/xbmc-addons/language/fa_IR/)\n" -"Language: fa_IR\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "پوشه" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "میزان نور محافظ" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "اسلاید" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "غیر فعال" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "ساده" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.fi_fi/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.fi_fi/strings.po deleted file mode 100644 index e404dd4ea..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.fi_fi/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-07-17 10:14+0000\n" -"Last-Translator: Oskari Lavinto \n" -"Language-Team: Finnish \n" -"Language: fi_fi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.13\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Näytönsäästäjä, joka näyttää kuvaesitystä" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Slideshow-näytönsäästäjä näyttää kuvaesityksen erilaisilla siirtymätehosteilla. Se voidaan määrittää esittämään musiikki- tai videokirjastojen fanart-kuvia tai tai valitun kansion kuvia." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Esityksen kuvalähde" - -msgctxt "#30001" -msgid "Folder" -msgstr "Kansio" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Videokirjaston fanart-kuvat" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Musiikkikirjaston fanart-kuvat" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Kuvakansio" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Himmennystaso" - -msgctxt "#30006" -msgid "Effect" -msgstr "Tehoste" - -msgctxt "#30007" -msgid "Slide" -msgstr "Liu'utus" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Ristihäivytys" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panorointi ja zoomaus" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Yksittäisen kuvan näyttöaika sekunteina" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Skaalaa kuvat koko ruutuun" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Näytä kuvan nimi" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Ei käytössä" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Käytä tiedoston nimeä" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Käytän kansion nimeä" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Näytä kuvat satunnaisessa järjestyksessä" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Käytä koko tiedostosijaintia" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Käytä kansion ja tiedoston nimeä" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Näytä kuvan päiväys" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Näytä lisää kuvatunnisteita" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Näytä musiikin tiedot toistettaessa ääntä" - -msgctxt "#30024" -msgid "Basic" -msgstr "Perus" - -msgctxt "#30025" -msgid "Additional" -msgstr "Laaja" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Jatka kuvaesitystä aiemmasta sijainnista" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Näytä taustakuva" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Käytä vinjetti-peittokuvaa" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.fo_fo/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.fo_fo/strings.po deleted file mode 100644 index 57e121a2b..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.fo_fo/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Faroese (http://www.transifex.com/projects/p/xbmc-addons/language/fo/)\n" -"Language: fo\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mappa" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Sløkt" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.fr_ca/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.fr_ca/strings.po deleted file mode 100644 index d0f065e5c..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.fr_ca/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: French (Canada) (http://www.transifex.com/projects/p/xbmc-addons/language/fr_CA/)\n" -"Language: fr_CA\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Économiseur d'écran affichant un diaporama" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "L'économiseur d'écran diaporama vous montrera un diaporama d'images avec différents effets de transition. Il peut être configuré pour afficher votre audiothèque, les fanarts de vos vidéos ou un dossier personnalisé d'images." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Source des images pour le diaporama" - -msgctxt "#30001" -msgid "Folder" -msgstr "Dossier" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart de la vidéo" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart de la musique" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Dossier des images" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Niveau d'atténuation" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effet" - -msgctxt "#30007" -msgid "Slide" -msgstr "Glisser" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Fondu" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panoramique et zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Durée d'affichage de chaque image en secondes" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Agrandir les images en plein écran" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Afficher le nom des images" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Désactivé" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Utiliser le nom du fichier" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Utiliser le nom du dossier" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Afficher les images dans un ordre aléatoire" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Utiliser le nom de dossier et de fichier" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Afficher la date des images" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Afficher les balises supplémentaires de l'image" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Afficher les infos de la musique lors de la lecture audio" - -msgctxt "#30024" -msgid "Basic" -msgstr "De base" - -msgctxt "#30025" -msgid "Additional" -msgstr "Supplémentaire" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Afficher une image d'arrière-plan" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.fr_fr/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.fr_fr/strings.po deleted file mode 100644 index 3f6a4b640..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.fr_fr/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-12-23 18:15+0000\n" -"Last-Translator: skypichat \n" -"Language-Team: French (France) \n" -"Language: fr_fr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.15\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Économiseur d'écran affichant un diaporama" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "L'économiseur Diaporama affiche un album photo en utilisant divers effets de transition. Il peut afficher la collection de fanarts vidéos ou musicaux, ou un dossier personnalisé d'images" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Source des images du diaporama" - -msgctxt "#30001" -msgid "Folder" -msgstr "Dossier" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart vidéo" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart musique" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Dossier d'images" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Niveau d'assombrissement" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effet" - -msgctxt "#30007" -msgid "Slide" -msgstr "Diaporama" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Fondu-enchaîné" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panoramique et zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Durée d'affichage de chaque image en secondes" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Redimensionner les images en plein écran" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Afficher le nom de l'image" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Désactivé" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Utiliser le nom du fichier" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Utiliser le nom du dossier" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Afficher les images dans un ordre aléatoire" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Utiliser le chemin complet" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Utiliser dossier et nom de fichier" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Afficher la date de l'image" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Afficher les informations additionnelles de l'image" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Afficher les informations sur la musique pendant la lecture audio" - -msgctxt "#30024" -msgid "Basic" -msgstr "Basique" - -msgctxt "#30025" -msgid "Additional" -msgstr "Additionnel" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Reprendre le diaporama à partir de la dernière position" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Afficher l'image d'arrière-plan" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Utiliser la superposition de vignettes" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.gl_es/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.gl_es/strings.po deleted file mode 100644 index e59c38036..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.gl_es/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Galician (http://www.transifex.com/projects/p/xbmc-addons/language/gl/)\n" -"Language: gl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "O gardapantallas que amosa unha presentación de imaxes" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "O gardapantallas Slideshow amosará unha presentación de imaxes empregando varios efectos de transición. Pode ser configurado para amosar os carteis das túas biblotecas de video ou música ou imaxes dun cartafol personalizado." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Fonte das imaxes para a presentación" - -msgctxt "#30001" -msgid "Folder" -msgstr "Cartafol" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Carteis de Video" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Carteis de Música" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Cartafol de Imaxes" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nivel de atenuación" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efecto" - -msgctxt "#30007" -msgid "Slide" -msgstr "Presentación" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Atenuación" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panorámica e ampliación" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "tempo en segundos nos que se amosará cada imaxe" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Escalar imaxes a pantalla completa" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Amosar o nome da imaxe" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Desactivado" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Usar o nome do ficheiro" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Usar o nome do cartafol" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Amosar imaxes ó chou" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Usar o cartafol- e o nome do ficheiro" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Amosar a data da imaxe" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Amosar etiquetas adicionais da imaxe" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Amosar a información de audio durante a reprodución" - -msgctxt "#30024" -msgid "Basic" -msgstr "Básico" - -msgctxt "#30025" -msgid "Additional" -msgstr "Adicional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Amosar unha imaxe de fondo" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.he_il/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.he_il/strings.po deleted file mode 100644 index 52787e604..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.he_il/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-08-03 14:29+0000\n" -"Last-Translator: Yaron Shahrabani \n" -"Language-Team: Hebrew (Israel) \n" -"Language: he_il\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "שומר מסך אשר יציג מצגת של תמונות" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "השומר מסך יציג לך מצגת של תמונות אשר עושה שימוש במעברים שונים. יש אפשרות להגדיר את תמונות הפאנארט של ספריות המוזיקה והוידאו, או תיקיית תמונות אישית" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "מקור תמונות המצגת" - -msgctxt "#30001" -msgid "Folder" -msgstr "תיקיה" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "וידאו פאנארט" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "מוזיקה פאנארט" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "תיקיית תמונות" - -msgctxt "#30005" -msgid "Dim level" -msgstr "רמת עמעום" - -msgctxt "#30006" -msgid "Effect" -msgstr "אפקט" - -msgctxt "#30007" -msgid "Slide" -msgstr "מצגת" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "התמזגות" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "תקריב וחיתוך מסך" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "כמות השניות כדי להציג כל תמונה" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "הגדל תמונות למסף מלא" - -msgctxt "#30012" -msgid "Display image name" -msgstr "הצג שם תמונה" - -msgctxt "#30013" -msgid "Disabled" -msgstr "מנוטרל" - -msgctxt "#30014" -msgid "Use filename" -msgstr "השתמש בשם קובץ" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "השתמש בשם תיקיה" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "הצגת התמונות בסדר אקראי" - -msgctxt "#30017" -msgid "Use full path" -msgstr "להשתמש בנתיב המלא" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "להשתמש בתיקייה- ובשם הקובץ" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "הצג תאריך תמונה" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "הצג תוויות תמונה נוספות" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "הצג פרטי מוזיקה בזמן ניגון אודיו" - -msgctxt "#30024" -msgid "Basic" -msgstr "בסיסי" - -msgctxt "#30025" -msgid "Additional" -msgstr "נוסף" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "להמשיך את המצגת מאין שהופסקה" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "הצג תמונת רקע" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.hi_in/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.hi_in/strings.po deleted file mode 100644 index 07ebf35a1..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.hi_in/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-07-22 14:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Hindi (India) \n" -"Language: hi_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "सक्रीनसेवर जो चित्रो के स्लीडेशो को प्रदर्शन करता है" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "स्लीडेशो चित्रो का सोर्स" - -msgctxt "#30001" -msgid "Folder" -msgstr "फोल्डर" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "वीडियो फ़ानर्ट" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "म्यूज़िक फ़ानर्ट" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "चित्र फ़ानर्ट" - -msgctxt "#30005" -msgid "Dim level" -msgstr "डिम का ताल" - -msgctxt "#30006" -msgid "Effect" -msgstr "एफेक्ट" - -msgctxt "#30007" -msgid "Slide" -msgstr "\tपरिकलन" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "क्रॉसफेड" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "पएन और ज़ूम करे" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "प्रतेक चित्र को प्रदर्शन करने मे सेकेंड्स की मात्रा" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "चित्रो को फुलस्क्रीन मे नापे" - -msgctxt "#30012" -msgid "Display image name" -msgstr "चित्र के नाम का प्रदर्सन करे" - -msgctxt "#30013" -msgid "Disabled" -msgstr "विकलांग" - -msgctxt "#30014" -msgid "Use filename" -msgstr "फाइलनेम का प्रयोग करे" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "फोल्डर के नेम का प्रयोग करे" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "चित्रो को यादृच्छिक तरीके से प्रदर्सन करे" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "चित्र के दिनांक का प्रदर्सन करे" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "अतिरिक्त पिक्चर टॅग्स का प्रदर्सन करे" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "ऑडियो प्लेबॅक के दौरान गाने की जानकारी दिखाए" - -msgctxt "#30024" -msgid "Basic" -msgstr "बुनियादी" - -msgctxt "#30025" -msgid "Additional" -msgstr "अतिरिक्त" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.hr_hr/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.hr_hr/strings.po deleted file mode 100644 index 43e568a5d..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.hr_hr/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Croatian (http://www.transifex.com/projects/p/xbmc-addons/language/hr/)\n" -"Language: hr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Čuvar zaslona koji prikazuje slikovnu prezentaciju" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Čuvar zaslona slikovne prezentacije prikazuje slikovnu prezentaciju uz razne prijelazne efekte. Može se podesiti da prikazuje Vašu bazu glazbenih ili video slika omota, ili odabranu mapu sa slikama" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Izvor slika slikovne prezentacije" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mapa" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video slika omota" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Glazbena slika omota" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Mapa sa slikama" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Razina zatamnjenja" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Prezentacija" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Pretapanje" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Obrezivanje i zumiranje" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Trajanje u sekundama prikaza svake slike" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Razvuci slike preko cijelog zaslona" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Prikaži naziv slike" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Onemogućen" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Koristi naziv datoteke" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Koristi naziv mape" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Prikaži slike naizmjeničnim odabirom" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Korisit mapu - i naziv datoteke" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Prikaži datum slike" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Prikaži dodatne oznake slike" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Prikaži glazbene informacije tijekom audio reprodukcije" - -msgctxt "#30024" -msgid "Basic" -msgstr "Osnovno" - -msgctxt "#30025" -msgid "Additional" -msgstr "Dodatno" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Prikaži sliku pozadine" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.hu_hu/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.hu_hu/strings.po deleted file mode 100644 index 4581e2a30..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.hu_hu/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Hungarian (http://www.transifex.com/projects/p/xbmc-addons/language/hu/)\n" -"Language: hu\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Képernyővédő ami diavetítést csinál" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "A Slideshow képernyővédő diavetítés-szerűen képeket mutat egymás után különböző váltási effektussal. Beállítható úgy hogy a médiatárból mutasson zenei-, vagy film fanartképeket, vagy egy egyedi mappából mutassa azokat." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Diavetítés képeinek elérhetősége" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mappa" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Videó Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Zene Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Kép mappa" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Elsötétítés szintje" - -msgctxt "#30006" -msgid "Effect" -msgstr "Hatás" - -msgctxt "#30007" -msgid "Slide" -msgstr "Csúszka" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Átmosás" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Pásztázás és nagyítás (Pan&Zoom)" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Képek megjelenítési ideje másodperceben" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Képek teljes képernyőre igazítása" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Kép nevének megjelenítése" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Letiltva" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Fájlnév használata" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Mappanév használata" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Képek véletlenszerű betöltése" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Képadatok megjelenítése" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Hozzáadott képelemek megjelenítése" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Zene infó megjelenítése lejátszás közben" - -msgctxt "#30024" -msgid "Basic" -msgstr "Alap" - -msgctxt "#30025" -msgid "Additional" -msgstr "Hozzáadott" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.id_id/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.id_id/strings.po deleted file mode 100644 index f36db9b2e..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.id_id/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Indonesian (http://www.transifex.com/projects/p/xbmc-addons/language/id/)\n" -"Language: id\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Folder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart Musik" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Tingkat redup" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Non Aktif" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Dasar" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.is_is/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.is_is/strings.po deleted file mode 100644 index 549629c40..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.is_is/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2023-09-25 12:11+0000\n" -"Last-Translator: Sveinn í Felli \n" -"Language-Team: Icelandic \n" -"Language: is_is\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Weblate 5.0.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Skjáhvíla sem sýnir skyggnusýningu mynda" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Þessi skjáhvíla sýnir þér skyggnusýningu af myndum með ýmsum myndbrellum. Það er hægt að stilla hana til að sýna aðdáendamyndir úr tónlistar eða myndskeiðasöfnum, eða úr sérsniðinni möppu með myndum" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Veita skyggnumynda" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mappa" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Aðdáendamyndir myndskeiða" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Aðdáendamyndir tónlistar" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Myndamappa" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Birtustilling" - -msgctxt "#30006" -msgid "Effect" -msgstr "Myndbrellur" - -msgctxt "#30007" -msgid "Slide" -msgstr "Skruna" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Krossblanda" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Hliðrun og aðdráttur" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Tími í sekúndum sem á að sýna hverja mynd" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Kvarða myndir á allan skjáinn" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Sýna nafn myndar" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Óvirkt" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Nota skráarheiti" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Nota möppuheiti" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Birta myndir í handahófskenndri röð" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Nota fulla slóð" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Nota möppu- og skráarnafn" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Sýna dagsetningu myndar" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Sýna frekari merkingar myndar" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Sýna tónlistarupplýsingar meðan lag er spilað" - -msgctxt "#30024" -msgid "Basic" -msgstr "Einfalt" - -msgctxt "#30025" -msgid "Additional" -msgstr "Meira" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Halda skyggnusýningu áfram úr síðustu stöðu" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Sýna bakgrunnsmynd" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Nota merki sem yfirlag" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.it_it/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.it_it/strings.po deleted file mode 100644 index d2c18cc43..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.it_it/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-08-30 20:14+0000\n" -"Last-Translator: Massimo Pissarello \n" -"Language-Team: Italian \n" -"Language: it_it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.14\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Salvaschermo che mostra una presentazione di immagini" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Il salvaschermo Slideshow mostra una presentazione di immagini usando diversi effetti di transizione. Può essere configurato per mostrare le fanart delle librerie musica o video, oppure una cartella di immagini specifica" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Fonte delle immagini" - -msgctxt "#30001" -msgid "Folder" -msgstr "Cartella" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart video" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart musica" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Cartella immagini" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Livello oscuramento" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effetto" - -msgctxt "#30007" -msgid "Slide" -msgstr "Diapositiva" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Dissolvenza incrociata" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panoramica e zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Numero di secondi per visualizzare ciascuna immagine" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Ridimensiona immagini a schermo intero" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Visualizza nome immagine" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Disabilitato" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Usa nome file" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Usa nome cartella" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Visualizza immagini in ordine casuale" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Usa percorso completo" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Usa cartella- e nome file" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Visualizza data immagine" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Visualizza tag aggiuntivi immagine" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Visualizza informazioni sulla musica durante la riproduzione audio" - -msgctxt "#30024" -msgid "Basic" -msgstr "Base" - -msgctxt "#30025" -msgid "Additional" -msgstr "Supplementare" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Riprendi presentazione dall'ultima posizione" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Visualizza immagine di sfondo" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Usa sovrapposizione vignettatura" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ja_jp/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ja_jp/strings.po deleted file mode 100644 index b5166edf1..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ja_jp/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-07-22 14:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Japanese \n" -"Language: ja_jp\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "画像をスライドショー表示するスクリーンセーバー" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "スライドショー画像" - -msgctxt "#30001" -msgid "Folder" -msgstr "フォルダー" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "映像ファンアート" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "音楽ファンアート" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "画像フォルダー" - -msgctxt "#30005" -msgid "Dim level" -msgstr "ぼかしレベル" - -msgctxt "#30006" -msgid "Effect" -msgstr "エフェクト" - -msgctxt "#30007" -msgid "Slide" -msgstr "スライド" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "クロスフェード" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "パン&ズーム" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "各画像を表示する秒数" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "イメージをフルスクリーンにスケーリング" - -msgctxt "#30012" -msgid "Display image name" -msgstr "イメージ名を表示" - -msgctxt "#30013" -msgid "Disabled" -msgstr "無効" - -msgctxt "#30014" -msgid "Use filename" -msgstr "ファイル名を使う" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "フォルダ名を使う" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "ランダムにイメージを表示する" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "画像の日付を表示" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "イメージタグの追加を表示" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "オーディオ再生時に音楽情報を表示" - -msgctxt "#30024" -msgid "Basic" -msgstr "標準" - -msgctxt "#30025" -msgid "Additional" -msgstr "追加" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ko_kr/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ko_kr/strings.po deleted file mode 100644 index 3bb7f8a8f..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ko_kr/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-07-01 15:22+0000\n" -"Last-Translator: Minho Park \n" -"Language-Team: Korean \n" -"Language: ko_kr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.13\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "이미지 슬라이드쇼를 보여주는 화면 보호기" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "슬라이드쇼 화면 보호기는 다양한 화면 전환효과를 이용해 이미지 슬라이드쇼를 보여줍니다. 음악 라이브러리, 비디오 팬아트, 사용자 폴더의 사진을 볼 수 있게 설정할 수 있습니다" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "슬라이드쇼 이미지 소스" - -msgctxt "#30001" -msgid "Folder" -msgstr "폴더" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "비디오 팬아트" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "음악 팬아트" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "이미지 폴더" - -msgctxt "#30005" -msgid "Dim level" -msgstr "어두운 정도" - -msgctxt "#30006" -msgid "Effect" -msgstr "효과" - -msgctxt "#30007" -msgid "Slide" -msgstr "슬라이드" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "크로스페이드" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "확대와 이동" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "각 이미지를 표시할 시간 (초)" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "전체화면 늘이기" - -msgctxt "#30012" -msgid "Display image name" -msgstr "이미지 이름 표시" - -msgctxt "#30013" -msgid "Disabled" -msgstr "사용 안 함" - -msgctxt "#30014" -msgid "Use filename" -msgstr "파일명 사용" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "폴더명 사용" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "무작위 순으로 이미지 표시" - -msgctxt "#30017" -msgid "Use full path" -msgstr "전체 경로 사용" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "사용자 폴더 - 파일 이름" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "사진 날짜 표시" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "부가 사진 태그 표시" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "오디오 재생중에 음악 정보 표시" - -msgctxt "#30024" -msgid "Basic" -msgstr "기본" - -msgctxt "#30025" -msgid "Additional" -msgstr "부가" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "이전 위치에서 슬라이드쇼 다시 시작" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "배경 사진 표시" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "비네트 오버레이 사용" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.lt_lt/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.lt_lt/strings.po deleted file mode 100644 index dc5688493..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.lt_lt/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Lithuanian (http://www.transifex.com/projects/p/xbmc-addons/language/lt/)\n" -"Language: lt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Ekrano užsklanda, kuri rodo skaidrių peržiūrą" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Skaidrių peržiūros ekrano užsklanda atvaizduoja skaidrių peržiūros vaizdus, naudojant įvairius perėjimo efektus. Jis konfigūruojamas, kad rodyti jūsų bibliotekos muzikos ar vaizdo FanArt arba pasirinktinį paveikslėlių aplanką" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Skaidrių peržiūros paveikslėlių šaltinis" - -msgctxt "#30001" -msgid "Folder" -msgstr "Aplankas" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Vaizdo Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Muzikos Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Paveikslėlių aplankas" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Pritemdymo lygis" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efektas" - -msgctxt "#30007" -msgid "Slide" -msgstr "Skaidrės" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Užslanka" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Perkelti ir padidinti" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Laikas sekundėmis, kiekvieno paveikslėlio rodymui" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Paveikslėlių mastelis visame ekrane" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Rodyti paveikslėlio pavadinimą" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Atjungta" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Naudoti failo pavadinimą" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Naudoti aplanko pavadinimą" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Rodytis paveikslėlius atsitiktine tvarka" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Naudoti aplanko ir failo vardą" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Rodyti nuotraukos datą" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Rodyti papildomas nuotraukų žymes" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Rodyti muzikos informaciją garso atkūrimo metu" - -msgctxt "#30024" -msgid "Basic" -msgstr "Pagrindinis" - -msgctxt "#30025" -msgid "Additional" -msgstr "Papildomas" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Rodyti fono paveikslėlį" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.lv_lv/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.lv_lv/strings.po deleted file mode 100644 index 282beab17..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.lv_lv/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Latvian (http://www.transifex.com/projects/p/xbmc-addons/language/lv/)\n" -"Language: lv\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Ekrānsaudzētājs, kas rāda attēlu slīdīti" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Slīdītes ekrānsaudzētājs parādīs slīdīti ar attēliem, izmantojot dažādus pārejas efektus. To var konfigurēt, lai rāda fanumākslu no mūzikas vai video bibliotekām, vai arī no pielāgotas attēlu mapes" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Slīdītes attēlu avots" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mape" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video fanumāksla" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Mūzikas fanumāksla" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Attēlu mape" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Tumšuma līmenis" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efekts" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Izslēgts" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Pamata" - -msgctxt "#30025" -msgid "Additional" -msgstr "Papildus" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.mi/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.mi/strings.po deleted file mode 100644 index 719a3ecd5..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.mi/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: Kodi add-ons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.mk_mk/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.mk_mk/strings.po deleted file mode 100644 index 1a8ca82cd..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.mk_mk/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Macedonian (http://www.transifex.com/projects/p/xbmc-addons/language/mk/)\n" -"Language: mk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Заштита на екран што покажува слајдшоу од слики" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Слајдшоу заштитата на екран ќе покаже слајд шоу на слики користејќи ефекти на транзиција. Може да биде конфигурирано да покажува и плакат од вашата музика или филмови или ваша папка со слики" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Извор на слики за слајдшоу" - -msgctxt "#30001" -msgid "Folder" -msgstr "Папки" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Видео плакати" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Плакати од музиката" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Папка со слики" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Ниво на стемнување" - -msgctxt "#30006" -msgid "Effect" -msgstr "Ефект" - -msgctxt "#30007" -msgid "Slide" -msgstr "Слајд" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Преклоп" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Пан и Зум" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Колку секунди да се прикаже секоја слика" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Оневозможено" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Основно" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ms_my/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ms_my/strings.po deleted file mode 100644 index a332ef8a7..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ms_my/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Malay (http://www.transifex.com/projects/p/xbmc-addons/language/ms/)\n" -"Language: ms\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Penyelamat skrin yang memaparkan paparan slaid imej" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Penyelamat skrin paparan slaid akan paparkan paparan slaid imej menggunakan pelabgai kesan peralihan. Ia boleh dikonfigur untuk tunjukkan pustaka muzik atau seni peminat anda, atau folder imej suai" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Sumber imej paparan slaid" - -msgctxt "#30001" -msgid "Folder" -msgstr "Folder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Seni Peminat" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Muzik Seni Peminat" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Folder Imej" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Aras malap" - -msgctxt "#30006" -msgid "Effect" -msgstr "Kesan" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slaid" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Resap Silang" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Lata dan zum" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Amaun saat untuk paparkan setiap imej" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Skala imej berskrin penuh" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Papar nama imej" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Dilumpuhkan" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Guna nama fail" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Guna nama folder" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Papar imej dalam tertib rawak" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Papar tarikh gambar" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Papar tag gambar tambahan" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Papar maklumat muzik semasa main balik audio" - -msgctxt "#30024" -msgid "Basic" -msgstr "Asas" - -msgctxt "#30025" -msgid "Additional" -msgstr "Tambahan" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.mt_mt/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.mt_mt/strings.po deleted file mode 100644 index 50466c926..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.mt_mt/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Maltese (http://www.transifex.com/projects/p/xbmc-addons/language/mt/)\n" -"Language: mt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Fowlder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Mhux attiv" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Bażiku" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.my_mm/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.my_mm/strings.po deleted file mode 100644 index fcbf6fa33..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.my_mm/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Burmese (http://www.transifex.com/projects/p/xbmc-addons/language/my/)\n" -"Language: my\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Screensaver သည်ပုံများကို slideshow အဖြစ်ပြသပေးသည်။" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Slideshow ပုံများ၏မူလရင်းမြစ်" - -msgctxt "#30001" -msgid "Folder" -msgstr "ဖိုလ်ဒါ" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "ဗွီဒီယို Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Music Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "ပုံဖိုလ်ဒါ" - -msgctxt "#30005" -msgid "Dim level" -msgstr "မှေးမှိန်စေသော အမှတ်" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effect" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "ပုံတစ်ပုံကိုပြရမည့်စက္ကန့်" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "ပုံအမည်ကိုဖော်ပြမည်" - -msgctxt "#30013" -msgid "Disabled" -msgstr "ပိတ်ထားမည်" - -msgctxt "#30014" -msgid "Use filename" -msgstr "ဖိုင်အမည်ကိုသုံးမည်" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "ဖိုလ်ဒါလ် အမည်ကိုသုံးမည်" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "ပုံ၏ရက်စွဲကိုပြမည်" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "အခြေခံ" - -msgctxt "#30025" -msgid "Additional" -msgstr "နောက်ထပ်" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.nb_no/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.nb_no/strings.po deleted file mode 100644 index c461d08a8..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.nb_no/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Norwegian (http://www.transifex.com/projects/p/xbmc-addons/language/no/)\n" -"Language: no\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Skjermbeskytter med lysbildefremvisning" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Denne skjermbeskytteren viser bilder med forskjellige overgangseffekter. Den kan konfigureres til å vise fanart fra musikk- eller videobiblioteket ditt, eller en mappe med bilder." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Kilde for bilder" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mappe" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Musikk Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Bilde-mappe" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Dim nivå" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Skli" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Crossfade" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Pan og zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Antall sekunder hvert bilde vises" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Forstørr bilder til fullskjerm" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Vis bildetittel" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Deaktivert" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Bruk filnavn" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Bruk mappenavn" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Vis bilder i tilfeldig rekkefølge" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Vis bildedato" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Vis flere bildetags" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Vis musikkinfo under audioavspilling" - -msgctxt "#30024" -msgid "Basic" -msgstr "Grunnleggende" - -msgctxt "#30025" -msgid "Additional" -msgstr "Ytterligere" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.nl_nl/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.nl_nl/strings.po deleted file mode 100644 index 12ed6d815..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.nl_nl/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-06-07 12:14+0000\n" -"Last-Translator: Pander \n" -"Language-Team: Dutch \n" -"Language: nl_nl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.12.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Schermbeveiliging die een diavoorstelling toont" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "De diavoorstellingsschermbeveiliging zal een diavoorstelling tonen met verschillende overgangseffecten. Je kunt een muziek- of videofanart uit je bibliotheek tonen of zelf een map opgeven die afbeeldingen bevat." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Locatie van diavoorstelling afbeeldingen" - -msgctxt "#30001" -msgid "Folder" -msgstr "Map" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Videofanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Muziekfanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Afbeeldingenmap" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Dimveau" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effect" - -msgctxt "#30007" -msgid "Slide" -msgstr "Dia" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Vloeiende overgang" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Draaien en zoomen" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Aantal seconden om elk plaatje te tonen" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "schaal foto's naar volledig scherm" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Weergeven foto naam" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Uitgeschakeld" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Gebruik bestandsnaam" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Gebruik mapnaam" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Toon afbeeldingen in willekeurige volgorde" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Gebruik volledig pad" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Gebruik map- en bestandsnaam" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Toon afbeeldingsdatum" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Toon extra afbeelding tags" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Toon muziekinformatie tijdens het afspelen" - -msgctxt "#30024" -msgid "Basic" -msgstr "Basis" - -msgctxt "#30025" -msgid "Additional" -msgstr "Extra" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Hervat diavoorstelling vanaf laatste positie" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Toon achtergrondafbeelding" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.pl_pl/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.pl_pl/strings.po deleted file mode 100644 index d85c1e22e..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.pl_pl/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-06-18 22:14+0000\n" -"Last-Translator: Marek Adamski \n" -"Language-Team: Polish \n" -"Language: pl_pl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.13\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Wygaszacz ekranu wyświetlający pokaz slajdów" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Ten wygaszacz ekranu wyświetla pokaz slajdów z różnymi efektami przejścia. Można go skonfigurować tak, aby wyświetlał fototapety filmów, muzyki lub obrazy z niestandardowego folderu" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Źródło obrazów dla pokazu slajdów" - -msgctxt "#30001" -msgid "Folder" -msgstr "Folder" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fototapeta wideo" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fototapeta muzyki" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Folder obrazów" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Poziom przyciemnienia" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Przesuwania" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Przejścia" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Efekt Kena Burnsa" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Liczba sekund wyświetlania każdego obrazu" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Dopasowuj obrazy do pełnego ekranu" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Wyświetlaj nazwę obrazu" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Nieaktywny" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Używaj nazwy pliku" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Używaj nazwy folderu" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Wyświetlaj obrazy w losowej kolejności" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Używaj pełnej ścieżki" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Używaj folderu i nazwy pliku" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Wyświetlaj datę obrazu" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Wyświetlaj dodatkowe znaczniki obrazu" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Wyświetlaj informacje o odtwarzanej muzyce" - -msgctxt "#30024" -msgid "Basic" -msgstr "Podstawowe" - -msgctxt "#30025" -msgid "Additional" -msgstr "Dodatkowe" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Wznów pokaz slajdów z ostatniej pozycji" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Pokazuj obraz tła" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "Używaj nakładki winiety" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.pt_br/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.pt_br/strings.po deleted file mode 100644 index 374754a34..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.pt_br/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/xbmc-addons/language/pt_BR/)\n" -"Language: pt_BR\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Proteção de tela que mostra uma apresentação de fotografias" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "O screensaver Slideshow irá mostrar-lhe uma apresentação de slides de suas fotografias usando diferentes efeitos de transição. Ele pode ser configurado para visualizar fanarts de sua biblioteca de músicas ou de vídeo, ou ainda utilizar uma pasta personalizada que contenha imagens" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Fonte de imagens para o slideshow" - -msgctxt "#30001" -msgid "Folder" -msgstr "Pasta" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart de Vídeo" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart de Música" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Pasta com Imagens" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nível de escurecimento" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efeito" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Crossfade" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Pan e zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Total de segundos para mostrar cada imagem" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Escalonar imagens em tela cheia" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Mostrar nome da imagem" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Desabilitado" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Usar Nome do Arquivo" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Usar Nome da Pasta" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Apresentar imagens em ordem aleatória" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Usar pasta-e nome do arquivo" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Mostrar data fotografia" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Mostrar tags adicionais fotografia" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Mostrar info de música durante reprodução de áudio" - -msgctxt "#30024" -msgid "Basic" -msgstr "Básico" - -msgctxt "#30025" -msgid "Additional" -msgstr "Adicional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Mostrar imagem de fundo" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.pt_pt/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.pt_pt/strings.po deleted file mode 100644 index 0f8b7585f..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.pt_pt/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Portuguese (http://www.transifex.com/projects/p/xbmc-addons/language/pt/)\n" -"Language: pt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Protecção de ecrã que mostra uma apresentação de slides" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "O protector de ecrã de slides irá mostrar-lhe imagens usando vários efeitos de transição. Pode ser configurada para mostrar as imagens de Fanart da sua biblioteca de música ou de vídeo, e também pode mostrar as imagens de uma pasta à sua escolha." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Origem das imagens de Slides" - -msgctxt "#30001" -msgid "Folder" -msgstr "Pasta" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Fanart de Vídeo" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Fanart de Música" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Pasta das imagens" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nível de esvanecimento" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efeito" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Sobreposição na transição" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Movimento e Ampliação" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Segundos a exibir cada imagem" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Graduar imagens para ecrã cheio" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Exibir nome da imagem" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Desligado" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Usar nome do ficheiro" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Usar nome da pasta" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Exibir imagens numa ordem aleatória" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Usar nome do ficheiro e da pasta" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Exibir data da imagem" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Exibir etiquetas adicionais da imagem" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Exibir info da música durante reprodução" - -msgctxt "#30024" -msgid "Basic" -msgstr "Básico" - -msgctxt "#30025" -msgid "Additional" -msgstr "Adicional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ro_ro/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ro_ro/strings.po deleted file mode 100644 index 3466dfb6b..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ro_ro/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-07-22 14:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Romanian \n" -"Language: ro_ro\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Protector ecran care afișează o diaporamă de imagini" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Protector ecran de tip diaporamă care va afișa imagini folosind diferite efecte de tranziție. Poate fi configurat să afișeze imagini creații artistice din mediateca audio sau video sau dintr-un dosar personalizat." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Sursă imagini diaporamă" - -msgctxt "#30001" -msgid "Folder" -msgstr "Dosar" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Creații artistice video" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Creații artistice muzică" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Dosar imagini" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nivel întunecare" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efect" - -msgctxt "#30007" -msgid "Slide" -msgstr "Glisează" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Suprapunere" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Deplasare şi mărire" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Câte secunde se afişează fiecare imagine" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Imagini afișate pe tot ecranul" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Afișare nume imagini" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Dezactivat" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Utilizează nume fișier" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Utilizează nume director" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Arată imaginile în ordine aleatoare" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Folosește dosar- și nume fișier" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Arata data imaginii" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Arata informații adiționale ale imaginii" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Arată informații despre muzică în timpul redării audio" - -msgctxt "#30024" -msgid "Basic" -msgstr "De bază" - -msgctxt "#30025" -msgid "Additional" -msgstr "Adițional" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ru_ru/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ru_ru/strings.po deleted file mode 100644 index 929cd1184..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ru_ru/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Russian (http://www.transifex.com/projects/p/xbmc-addons/language/ru/)\n" -"Language: ru\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Заставка, показывающая слайд-шоу из изображений" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Эта заставка показывается слайд-шоу из изображений, используя различные эффекты переходов. Заставку можно настроить для показ фанарта для музыки и видео из вашей библиотеки мультимедиа или изображениям из заданной папки." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Источник изображений для слайд-шоу" - -msgctxt "#30001" -msgid "Folder" -msgstr "Папка" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Фанарт видео" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Фанарт музыки" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Папка изображений" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Уровень затемнения" - -msgctxt "#30006" -msgid "Effect" -msgstr "Эффект" - -msgctxt "#30007" -msgid "Slide" -msgstr "Слайды" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Затухание" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Панорамирование и масштабирование" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Сколько секунд отображать каждый слайд" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Растянуть изображение в полный экран" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Показывать имя изображения" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Откл." - -msgctxt "#30014" -msgid "Use filename" -msgstr "Использовать имя файла" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Использовать имя папки" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Показывать изображения в случайном порядке" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Показывать дату изображения" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Показывать доролнительные данные изображения" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Отображение информации о музыке во время воспроизведения аудио" - -msgctxt "#30024" -msgid "Basic" -msgstr "Базовый" - -msgctxt "#30025" -msgid "Additional" -msgstr "Дополнительно" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.sk_sk/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.sk_sk/strings.po deleted file mode 100644 index 71c7dfd50..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.sk_sk/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2023-03-04 09:16+0000\n" -"Last-Translator: Patrik Špaňo \n" -"Language-Team: Slovak \n" -"Language: sk_sk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 4.15.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Šetrič obrazovky zobrazujúci prezentáciu obrázkov" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Šetrič obrazovky Prezentácia vám zobrazí prezentáciu obrázkov s použitím rôznych prechodových efektov. Môže byť nastavený na zobrazovanie vašej knižnice hudby alebo video fanartov, alebo vlastného priečinku s obrázkami" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Zdroj obrázkov prezentácie" - -msgctxt "#30001" -msgid "Folder" -msgstr "Priečinok" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Hudobný Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Priečinok obrázkov" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Úroveň zatemnenia" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Diapozitívy" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Prelínanie" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Postupné približovanie" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Počet sekúnd, počas ktorých bude zobrazený obrázok" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Zmenšiť obrázky na celú obrazovku" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Zobraziť názov obrázku" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Zakázaný" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Použiť názov súboru" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Použiť názov priečinku" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Zobrazovať obrázky v náhodnom poradí" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Zobraziť dátum obrázku" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Zobraziť dodatočné značky obrázkov" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Zobraziť informácie o hudbe počas prehrávania audia" - -msgctxt "#30024" -msgid "Basic" -msgstr "Základné" - -msgctxt "#30025" -msgid "Additional" -msgstr "Dodatočné" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.sl_si/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.sl_si/strings.po deleted file mode 100644 index 527ed81cd..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.sl_si/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Slovenian (http://www.transifex.com/projects/p/xbmc-addons/language/sl/)\n" -"Language: sl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Ohranjevalnik zaslona, ki prikazuje diaprojekcijo slik" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Predvajalnik slik prikaže diaprojekcijo slik z uporabo različnih učinkov prehoda. To je mogoče nastaviti tako, da se prikažejo ozadja knjižnice glasbe ali videa alislike iz izbrane mape." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Vir slik za projekcijo" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mapa" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Grafike filmov" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Grafike glasbe" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Mapa s slikami" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nivo zatemnitve" - -msgctxt "#30006" -msgid "Effect" -msgstr "Učinek" - -msgctxt "#30007" -msgid "Slide" -msgstr "Drsenje" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Prelivanje" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Premik in povečava" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Čas prikaza posamezne slike" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Spremeni velikost slik v celozaslonskem načinu" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Prikaži ime slike" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Onemogočeno" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Uporabi ime datoteke" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Uporabi ime mape" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Prikaži slike v naključnem zaporedju" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Uporabi ime mape in datoteke" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Prikaži datum slike" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Prikaži dodatne informacije o sliki" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Prikaži informacije o glasbi med njenim predvajanjem" - -msgctxt "#30024" -msgid "Basic" -msgstr "Osnovno" - -msgctxt "#30025" -msgid "Additional" -msgstr "Dodatno" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.sq_al/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.sq_al/strings.po deleted file mode 100644 index 97eb39f9d..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.sq_al/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Albanian (http://www.transifex.com/projects/p/xbmc-addons/language/sq/)\n" -"Language: sq\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Ruajtës ekrani, që paraqet një shfaqje në seri ‘slideshow’ të imazheve" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Ruajtësi i ekranit ‘Slideshow’ do t'ju paraqesë një shfaqje në seri të imazheve, duke përdorur efekte të ndryshme tranzicioni. Mund të konfigurohet që të shfaqë sfonde të koleksiont të muzikës apo vidjove tuaja, ose imazhe prej një dosjeje të caktuar" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Burimi i imazheve të shfaqjes në seri 'slideshow'" - -msgctxt "#30001" -msgid "Folder" -msgstr "Dosje" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Sfonde për Vidjot" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Sfonde për Muzikën" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Dosja e Imazheve" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Niveli i errësimit" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effekti" - -msgctxt "#30007" -msgid "Slide" -msgstr "Slide" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Crossfade" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Pan dhe zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Koha në sekonda për të treguar çdo imazh" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Shkallëzo imazhet në ekran të plotë" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Trego emrin e imazhit" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Deaktivuar" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Përdor emrin e skedës" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Përdor emrin e dosjes" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Trego imazhe në një rend të rastësishëm" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Trego datën e fotografisë" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Trego tags të tjera shtuëse" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Trego informacionet e muzikës gjatë ridëgjimit" - -msgctxt "#30024" -msgid "Basic" -msgstr "Themelor" - -msgctxt "#30025" -msgid "Additional" -msgstr "Plotsues" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.sr_rs/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.sr_rs/strings.po deleted file mode 100644 index d3f880d41..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.sr_rs/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Serbian (Cyrillic) (http://www.transifex.com/projects/p/xbmc-addons/language/sr_RS/)\n" -"Language: sr_RS\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Фасцикле" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Ниво затамљења" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Онемогућено" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Основно" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.sr_rs@latin/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.sr_rs@latin/strings.po deleted file mode 100644 index e093ec0f9..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.sr_rs@latin/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Serbian (http://www.transifex.com/projects/p/xbmc-addons/language/sr/)\n" -"Language: sr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Fascikle" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Onemogućeno" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "Osnovno" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.sv_se/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.sv_se/strings.po deleted file mode 100644 index ccb62d406..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.sv_se/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2023-10-24 13:11+0000\n" -"Last-Translator: Robert Bylin \n" -"Language-Team: Swedish \n" -"Language: sv_se\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.0.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "En skärmsläckare som visar ett bildspel" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Bildspel är en skärmsläckare som visar ett bildspel med olika övergångseffekter. Den kan ställas in för att visa din musik- eller video-fanart från biblioteket eller en utvald mapp med bilder" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Bildspelskälla" - -msgctxt "#30001" -msgid "Folder" -msgstr "Mapp" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Videofanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Musikfanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Bildmapp" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Nedtoningsnivå" - -msgctxt "#30006" -msgid "Effect" -msgstr "Effekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Bildspel" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Övertoning" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Panorera och zooma" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Antal sekunder att visa varje bild" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Skala bilden till helskärm" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Visa bildnamnet" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Inaktiverad" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Använd filnamnet" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Använd mappnamnet" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Visa bilder i slumpmässig ordning" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Använd mapp- och filnamn" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Visa bilddatum" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Visa ytterligare bildtaggar" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Visa musikinfo under uppspelning" - -msgctxt "#30024" -msgid "Basic" -msgstr "Grundläggande" - -msgctxt "#30025" -msgid "Additional" -msgstr "Ytterligare" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.ta_in/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.ta_in/strings.po deleted file mode 100644 index b37942922..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.ta_in/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-07-22 14:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Tamil (India) \n" -"Language: ta_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "படகாட்சி திரைகாப்பன்" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "படகாட்சி திரைகாப்பன் பல்வேறு படங்களை பல மாற்ற விளைவுகளை பயன்படுத்தி படகாட்சி செய்யும். இது உங்கள் நூலகங்கள் இசை அல்லது வீடியோ விசிறிபடம் அல்லது படக்கொப்புரை படங்களை காட்சியில் காண்பிக்குமாறு செய்யமுடியும்." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "படக்காட்சி படங்களின் மூலம்" - -msgctxt "#30001" -msgid "Folder" -msgstr "கோப்புறை" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "நிகழ்பட விசிறிபடம்" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "இசை விசிறிபடம்" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "பட கோப்புறை" - -msgctxt "#30005" -msgid "Dim level" -msgstr "மங்கு நிலை" - -msgctxt "#30006" -msgid "Effect" -msgstr "விளைவு" - -msgctxt "#30007" -msgid "Slide" -msgstr "சறுக்கு" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "குறுக்குமங்கு" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "இடவல நகர்வு மற்றும் பெரிதாக்கு" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "அடுத்து படம் காண்பிக்க இத்தனை நொடிகள்" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "படங்களை முழுத்திரை அளவுக்கு மாற்று" - -msgctxt "#30012" -msgid "Display image name" -msgstr "பட பெயரை காட்டு" - -msgctxt "#30013" -msgid "Disabled" -msgstr "முடக்கப்பட்டன" - -msgctxt "#30014" -msgid "Use filename" -msgstr "கொப்புபெயரி உபயோகி" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "கோப்புறை பெயரை உபயோகி" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "சீரற்ற வரிசையில் படங்களை காண்பிக்க" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "பட தேதியை காட்டு" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "கூடுதல் படத்தை குறிச்சொற்களை காட்டு" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "ஆடியோ பின்னணி போது இசை தகவல் காட்டு" - -msgctxt "#30024" -msgid "Basic" -msgstr "அடிப்படை" - -msgctxt "#30025" -msgid "Additional" -msgstr "கூடுதலான" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.tg_tj/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.tg_tj/strings.po deleted file mode 100644 index 5c3a0d6ec..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.tg_tj/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: Kodi add-ons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: tg_tj\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.th_th/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.th_th/strings.po deleted file mode 100644 index 65043777f..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.th_th/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Thai (http://www.transifex.com/projects/p/xbmc-addons/language/th/)\n" -"Language: th\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "โฟลเดอร์" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "ระดับการหรี่หน้าจอ" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "เลื่อน" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "ปิดการใช้งาน" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "เบื้องต้น" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.tr_tr/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.tr_tr/strings.po deleted file mode 100644 index c94660c60..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.tr_tr/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Turkish (http://www.transifex.com/projects/p/xbmc-addons/language/tr/)\n" -"Language: tr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Resim slayt şovu gösteren ekran koruyucu" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Slideshow ekran koruyucu size farklı geçiş efektleriyle resim gösterisi yapar. Müzik kütüphanenizden veya video fanart yada özel bir klasördeki resimlerden seçilebilir." - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Slayt gösterisi resimleri kaynağı" - -msgctxt "#30001" -msgid "Folder" -msgstr "Dizin" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Müzik Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Resim Klasörü" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Karatma düzeyi" - -msgctxt "#30006" -msgid "Effect" -msgstr "Efekt" - -msgctxt "#30007" -msgid "Slide" -msgstr "Kaydır" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Çapraz geçiş" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Kaydır ve yakınlaştır" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Her resim için görüntüleme zamanı miktarı" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Resimleri tam ekrana ölçeklendir" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Resim ismini göster" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Devre dışı" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Dosya ismini kullan" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Dizin ismini kullan" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Resimleri rastgele sırada göster" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "Klasör ve dosya adı kullan" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Fotoğrafın tarihini göster" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Diğer resim etiketlerini göster" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Ses çalma sırasında müzik bilgisini göster" - -msgctxt "#30024" -msgid "Basic" -msgstr "Temel Bilgiler" - -msgctxt "#30025" -msgid "Additional" -msgstr "Ek" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Arka plan resmini görüntüle" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.uk_ua/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.uk_ua/strings.po deleted file mode 100644 index fc966db56..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.uk_ua/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-05-11 22:14+0000\n" -"Last-Translator: A. \n" -"Language-Team: Ukrainian \n" -"Language: uk_ua\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.12.1\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Заставка, що відображує слайд-шоу зі зображень" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Заставка \"Слайд-шоу\" показує зображення по черзі, використовуючи різноманітні ефекти переходу. Заставка може бути налаштована на показ зображень з різних джерел: обкладинок відео фільмів, обкладинок музичних альбомів або будь-яких фото із вказаної користувачем теки" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Джерело зображень для слайдшоу" - -msgctxt "#30001" -msgid "Folder" -msgstr "Папка" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Обкладинки відео фільмів" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Обкладинки музичних альбомів" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Папка із зображеннями" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Рівень затемнення" - -msgctxt "#30006" -msgid "Effect" -msgstr "Ефект" - -msgctxt "#30007" -msgid "Slide" -msgstr "Слайд" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Кросфейд" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Панорамування та масштабування" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Кількість секунд для показу кожного малюнку" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Розширити на весь екран" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Показувати назву малюнка" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Вимкн." - -msgctxt "#30014" -msgid "Use filename" -msgstr "Використати назву файлу" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Використовувати назву теки" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Показувати малюнки у випадковому порядку" - -msgctxt "#30017" -msgid "Use full path" -msgstr "Застосувати повний шлях" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Показати дані малюнка" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Показати додаткові мітки малюнка" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Показувати інформацію про малюнок під час програвання аудіо" - -msgctxt "#30024" -msgid "Basic" -msgstr "Базовий" - -msgctxt "#30025" -msgid "Additional" -msgstr "Додатково" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "Відновити слайд-шоу з останньої позиції" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "Показувати зображення тла" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.uz_uz/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.uz_uz/strings.po deleted file mode 100644 index a3b1f4c92..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.uz_uz/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Uzbek (http://www.transifex.com/projects/p/xbmc-addons/language/uz/)\n" -"Language: uz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "" - -msgctxt "#30001" -msgid "Folder" -msgstr "Jild" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "" - -msgctxt "#30005" -msgid "Dim level" -msgstr "" - -msgctxt "#30006" -msgid "Effect" -msgstr "" - -msgctxt "#30007" -msgid "Slide" -msgstr "" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "" - -msgctxt "#30012" -msgid "Display image name" -msgstr "" - -msgctxt "#30013" -msgid "Disabled" -msgstr "" - -msgctxt "#30014" -msgid "Use filename" -msgstr "" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "" - -msgctxt "#30024" -msgid "Basic" -msgstr "" - -msgctxt "#30025" -msgid "Additional" -msgstr "" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.vi_vn/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.vi_vn/strings.po deleted file mode 100644 index 48619d46f..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.vi_vn/strings.po +++ /dev/null @@ -1,136 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Vietnamese (http://www.transifex.com/projects/p/xbmc-addons/language/vi/)\n" -"Language: vi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "Trình bảo vệ màn hình giống như một trình chiếu ảnh" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "Trình bảo vệ màn hình Slideshow sẽ giúp bạn trình chiếu bộ sưu tập ảnh với các hiệu ứng chuyển đổi đẹp mắt. Bạn có thể tuỳ chỉnh để hiện thông tin về bài hát, video, fanart hoặc một thư mục chứa ảnh bất kì" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "Nguồn ảnh trình chiếu" - -msgctxt "#30001" -msgid "Folder" -msgstr "Thư mục" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "Video Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "Music Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "Thư mục ảnh" - -msgctxt "#30005" -msgid "Dim level" -msgstr "Độ mờ" - -msgctxt "#30006" -msgid "Effect" -msgstr "Hiệu ứng" - -msgctxt "#30007" -msgid "Slide" -msgstr "Ảnh" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "Đan xen dần" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "Di chuyển và zoom" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "Số giây chờ hiển thị mỗi ảnh" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "Giãn hình toàn màn hình" - -msgctxt "#30012" -msgid "Display image name" -msgstr "Hiện tên ảnh" - -msgctxt "#30013" -msgid "Disabled" -msgstr "Đã tắt" - -msgctxt "#30014" -msgid "Use filename" -msgstr "Sử dụng file" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "Sử dụng thư mục" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "Hiện ảnh theo thứ tự ngẫu nhiên" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "Hiện ngày tạo ảnh" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "Hiện thêm các thẻ khác" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "Hiện thông tin bài hát trong lúc nghe" - -msgctxt "#30024" -msgid "Basic" -msgstr "Cơ bản" - -msgctxt "#30025" -msgid "Additional" -msgstr "Thêm" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.zh_cn/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.zh_cn/strings.po deleted file mode 100644 index 31357b541..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.zh_cn/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2022-07-19 09:14+0000\n" -"Last-Translator: taxigps \n" -"Language-Team: Chinese (China) \n" -"Language: zh_cn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.13\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "幻灯片屏幕保护程序" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "幻灯片屏保将使用各种过渡效果以幻灯片形式显示您的图片。你可以设置它显示音乐或视频资料库同人画,或某个文件夹中的图片" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "幻灯片图片来源" - -msgctxt "#30001" -msgid "Folder" -msgstr "文件夹" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "视频同人画" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "音乐同人画" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "图片文件夹" - -msgctxt "#30005" -msgid "Dim level" -msgstr "变暗级别" - -msgctxt "#30006" -msgid "Effect" -msgstr "特效" - -msgctxt "#30007" -msgid "Slide" -msgstr "幻灯片" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "淡入淡出" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "平移缩放" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "图片显示时间" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "图片缩放为全屏" - -msgctxt "#30012" -msgid "Display image name" -msgstr "显示图片名" - -msgctxt "#30013" -msgid "Disabled" -msgstr "禁用" - -msgctxt "#30014" -msgid "Use filename" -msgstr "使用文件名" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "使用文件夹名" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "以随机顺序显示图片" - -msgctxt "#30017" -msgid "Use full path" -msgstr "使用完整路径" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "使用文件夹和文件名" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "显示图片日期" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "显示附加图片标签" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "在音频播放时显示音乐信息" - -msgctxt "#30024" -msgid "Basic" -msgstr "基本信息" - -msgctxt "#30025" -msgid "Additional" -msgstr "附加的" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "从最后位置恢复幻灯片" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "显示背景图片" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "使用渐晕覆盖" diff --git a/screensaver.picture.slideshow/resources/language/resource.language.zh_tw/strings.po b/screensaver.picture.slideshow/resources/language/resource.language.zh_tw/strings.po deleted file mode 100644 index 043c01dd6..000000000 --- a/screensaver.picture.slideshow/resources/language/resource.language.zh_tw/strings.po +++ /dev/null @@ -1,137 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Picture Slideshow Screensaver -# Addon id: screensaver.picture.slideshow -# Addon Provider: Team-Kodi -msgid "" -msgstr "" -"Project-Id-Version: XBMC Addons\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2021-11-09 11:30+0000\n" -"Last-Translator: JuenTingShie \n" -"Language-Team: Chinese (Taiwan) \n" -"Language: zh_tw\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.8.1\n" - -msgctxt "Addon Summary" -msgid "Screensaver that displays an image slideshow" -msgstr "顯示圖片幻燈秀的螢幕保護程式" - -msgctxt "Addon Description" -msgid "The Slideshow screensaver will show you a slide show of images using various transition effects. It can be configured to show your libraries music or video fanart, or a custom folder of images" -msgstr "幻燈片屏幕保護程序可以顯示使用各種過場效果的幻燈片播放。它可以被配置為顯示您的圖書館音樂模式或視頻圖庫,或自行定義的文件夾中的圖像" - -msgctxt "#30000" -msgid "Source of slideshow images" -msgstr "幻燈秀圖片來源" - -msgctxt "#30001" -msgid "Folder" -msgstr "資料夾" - -msgctxt "#30002" -msgid "Video Fanart" -msgstr "視訊 Fanart" - -msgctxt "#30003" -msgid "Music Fanart" -msgstr "音片 Fanart" - -msgctxt "#30004" -msgid "Image Folder" -msgstr "圖片資料夾" - -msgctxt "#30005" -msgid "Dim level" -msgstr "暗化等級" - -msgctxt "#30006" -msgid "Effect" -msgstr "效果" - -msgctxt "#30007" -msgid "Slide" -msgstr "幻燈片" - -msgctxt "#30008" -msgid "Crossfade" -msgstr "淡入淡出" - -msgctxt "#30009" -msgid "Pan and zoom" -msgstr "平移和縮放" - -msgctxt "#30010" -msgid "Amount of seconds to display each image" -msgstr "顯示每張圖片的秒數" - -msgctxt "#30011" -msgid "Scale images fullscreen" -msgstr "等比例圖片放大至全螢幕" - -msgctxt "#30012" -msgid "Display image name" -msgstr "顯示圖片名稱" - -msgctxt "#30013" -msgid "Disabled" -msgstr "關閉" - -msgctxt "#30014" -msgid "Use filename" -msgstr "使用檔案名稱" - -msgctxt "#30015" -msgid "Use foldername" -msgstr "使用資料夾名稱" - -msgctxt "#30016" -msgid "Display images in random order" -msgstr "以隨機順序顯示圖片" - -msgctxt "#30017" -msgid "Use full path" -msgstr "" - -# empty string with id 30017 -msgctxt "#30018" -msgid "Use folder- and filename" -msgstr "" - -# empty strings from id 30019 to 30020 -msgctxt "#30021" -msgid "Display picture date" -msgstr "顯示照片日期" - -msgctxt "#30022" -msgid "Display additional picture tags" -msgstr "顯示額外的照片標籤" - -msgctxt "#30023" -msgid "Display music info during audio playback" -msgstr "在音頻撥放時顯示音樂資訊" - -msgctxt "#30024" -msgid "Basic" -msgstr "基本" - -msgctxt "#30025" -msgid "Additional" -msgstr "額外的" - -msgctxt "#30026" -msgid "Resume slideshow from last position" -msgstr "" - -# empty strings from id 30027 to 30028 -msgctxt "#30029" -msgid "Display background picture" -msgstr "" - -msgctxt "#30030" -msgid "Use vignette overlay" -msgstr "" diff --git a/screensaver.picture.slideshow/resources/settings.xml b/screensaver.picture.slideshow/resources/settings.xml deleted file mode 100644 index b1ff7e1b7..000000000 --- a/screensaver.picture.slideshow/resources/settings.xml +++ /dev/null @@ -1,159 +0,0 @@ - - -
- - - - 0 - 0 - - - - - - - - - - - 0 - - - false - true - - pictures - - - - - 2 - - - - 30001 - - - - - - 0 - 10 - - - 2 - 1 - 100 - - - - 0 - 100 - - false - - - 0 - 1 - 100 - - - - 0 - 2 - - - - - - - - - - - - 0 - false - - - - - - - - 0 - true - - - - 0 - true - - - - 0 - false - - - false - - - - - - - - 0 - true - - - false - - - - - - 0 - false - - - false - - - - - - - - 0 - 0 - - - - - - - - - - - - - 0 - false - - - - 0 - false - - - - 0 - false - - - - -
-
diff --git a/screensaver.picture.slideshow/resources/skins/default/1080i/script-python-slideshow.xml b/screensaver.picture.slideshow/resources/skins/default/1080i/script-python-slideshow.xml deleted file mode 100644 index bcd29a2cc..000000000 --- a/screensaver.picture.slideshow/resources/skins/default/1080i/script-python-slideshow.xml +++ /dev/null @@ -1,315 +0,0 @@ - - - 6 - - - 0 - 0 - 1920 - 1080 - screensaver-black.png - WindowOpen - - - - - - - String.IsEqual(Window.Property(SlideView.Background),show) - - - - - - - - - - 0 - 0 - 1920 - 1080 - - scale - 0 - - - - - - - - - - - - 0 - 0 - 1920 - 1080 - - scale - 0 - - - - Conditional - 0 - 0 - 1920 - 1080 - screensaver-black.png - - - - - - - - - - - - 0 - 0 - 1920 - 1080 - - keep - 0 - - - - - - - - - - - - - - - - - - 0 - 0 - 1920 - 1080 - - keep - 0 - - - - - - - - - - - - - - - - - - 0 - 0 - 1920 - 1080 - - scale - 0 - - - - - - - - - - - - - - - - - - 0 - 0 - 1920 - 1080 - - scale - 0 - - - - - - - - - - 0 - 250 - auto - auto - overlayfade.png - Conditional - String.IsEqual(Window.Property(SlideView.Overlay),show) - - - 1875 - 992 - 900 - 30 - font14 - right - F0FFFFFF - F0000000 - - - 1875 - 1030 - 900 - 30 - font13 - right - C0FFFFFF - F0000000 - - - 45 - 925 - 900 - 135 - font13 - left - C0FFFFFF - C0000000 - ![Player.HasAudio + String.IsEqual(Window.Property(SlideView.Music),show)] - - - 30 - 862 - Conditional - Player.HasAudio + String.IsEqual(Window.Property(SlideView.Music),show) - - 0 - -7 - 210 - 210 - $INFO[MusicPlayer.Cover] - scale - - - 240 - 60 - 900 - 38 - left - font13 - - FFFFFFFF - FF000000 - - - 240 - 95 - 900 - 38 - left - font13 - - FFFFFFFF - FF000000 - - - 240 - 130 - 900 - 38 - left - font13 - - FFFFFFFF - FF000000 - - - 240 - 160 - - 0 - 12 - 150 - 30 - font13 - left - center - - FFFFFFFF - FF000000 - - - Progressbar - 128 - 18 - 300 - 21 - Player.Progress - screensaver-progress.png - screensaver-progress.png - - - - - - 555 - 12 - 150 - 30 - font13 - right - center - - FFFFFFFF - FF000000 - - - - - - 729 - 309 - 461 - 461 - keep - 0 - screensaver-splash.png - Conditional - - - - - - - !String.IsEqual(Window.Property(SlideView.Splash),hide) - - - 0 - 0 - 1920 - 1080 - screensaver-black.png - $INFO[Window.Property(SlideView.Dim)] - - - diff --git a/screensaver.picture.slideshow/resources/skins/default/media/overlayfade.png b/screensaver.picture.slideshow/resources/skins/default/media/overlayfade.png deleted file mode 100644 index 6a0b5b3b2..000000000 Binary files a/screensaver.picture.slideshow/resources/skins/default/media/overlayfade.png and /dev/null differ diff --git a/screensaver.picture.slideshow/resources/skins/default/media/screensaver-black.png b/screensaver.picture.slideshow/resources/skins/default/media/screensaver-black.png deleted file mode 100644 index fe4043ecf..000000000 Binary files a/screensaver.picture.slideshow/resources/skins/default/media/screensaver-black.png and /dev/null differ diff --git a/screensaver.picture.slideshow/resources/skins/default/media/screensaver-progress.png b/screensaver.picture.slideshow/resources/skins/default/media/screensaver-progress.png deleted file mode 100644 index 27cfd491a..000000000 Binary files a/screensaver.picture.slideshow/resources/skins/default/media/screensaver-progress.png and /dev/null differ diff --git a/screensaver.picture.slideshow/resources/skins/default/media/screensaver-splash.png b/screensaver.picture.slideshow/resources/skins/default/media/screensaver-splash.png deleted file mode 100644 index f96605cfd..000000000 Binary files a/screensaver.picture.slideshow/resources/skins/default/media/screensaver-splash.png and /dev/null differ diff --git a/script.areace/LICENSE.txt b/script.areace/LICENSE.txt deleted file mode 100644 index 0de60059e..000000000 --- a/script.areace/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Dmytro Pustovit - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/script.areace/addon.xml b/script.areace/addon.xml deleted file mode 100644 index 80054a492..000000000 --- a/script.areace/addon.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - executable - - - scrapes live sport event links from Telegram and seamlessly opens them in the Acestream player - Tired of the endless hunt for live sports event streams? "Are Ace" simplifies the process by scanning Telegram channels and groups known for sharing sports event links, collecting them for you. With an easy integration to the Acestream media player, it ensures you have quick access to high-quality, buffer-free sports event streams. Stay updated effortlessly, and enjoy a user-friendly interface for a seamless sports viewing experience. Say goodbye to the hassle of link searching, and say hello to the convenience of "Are Ace". - MIT - android - https://github.com/pustovitDmytro/areace - dipustovit@gmail.com - - resources/icon.png - resources/fanart.jpg - - - diff --git a/script.areace/resources/data_loader.py b/script.areace/resources/data_loader.py deleted file mode 100644 index 08120a3b6..000000000 --- a/script.areace/resources/data_loader.py +++ /dev/null @@ -1,18 +0,0 @@ -import xbmcaddon -from resources.telegram import fetch - -ADDON = xbmcaddon.Addon() - -def load_telegram(settings): - enabled = settings.getBool('telegram.isEnabled') - if (not enabled): return [] - opts = { - 'token': settings.getString('telegram.token'), - 'offset': settings.getInt('telegram.offset') - } - return fetch(opts) - - -def load_data(): - settings = ADDON.getSettings() - return load_telegram(settings) \ No newline at end of file diff --git a/script.areace/resources/fanart.jpg b/script.areace/resources/fanart.jpg deleted file mode 100644 index 9053d63af..000000000 Binary files a/script.areace/resources/fanart.jpg and /dev/null differ diff --git a/script.areace/resources/icon.png b/script.areace/resources/icon.png deleted file mode 100644 index 91d586b94..000000000 Binary files a/script.areace/resources/icon.png and /dev/null differ diff --git a/script.areace/resources/main_dialog.py b/script.areace/resources/main_dialog.py deleted file mode 100644 index b2617becf..000000000 --- a/script.areace/resources/main_dialog.py +++ /dev/null @@ -1,11 +0,0 @@ -import xbmc, xbmcgui -from resources.data_loader import load_data - -def main_dialog(): - dialog = xbmcgui.Dialog() - items = load_data() - selected=dialog.select("Streams", list([x['text'] for x in items])) - if selected>=0: - item = items[selected] - cmd = 'StartAndroidActivity("","org.acestream.action.start_content","","acestream:?content_id=%s")' % item['acestream_id'] - xbmc.executebuiltin(cmd) \ No newline at end of file diff --git a/script.areace/resources/settings.xml b/script.areace/resources/settings.xml deleted file mode 100644 index f412b5412..000000000 --- a/script.areace/resources/settings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/script.areace/resources/telegram.py b/script.areace/resources/telegram.py deleted file mode 100644 index 1b3997cfa..000000000 --- a/script.areace/resources/telegram.py +++ /dev/null @@ -1,26 +0,0 @@ -import requests -import re -import datetime as dtm - -host="https://api.telegram.org/" - -def fetch(config): - results=[] - params = dict(offset=config['offset']) - url=host+'bot'+config['token']+'/getUpdates' - res = requests.get(url, params) - json=res.json() - for update in json['result']: - if(not 'channel_post' in update): continue - if(not 'text' in update['channel_post']): continue - text = update['channel_post']['text'] - timestamp = update['channel_post']['date'] - date = dtm.datetime.fromtimestamp(timestamp) - obj={'text':text,'date':date} - match = re.search(r'[0-9a-f]{40}', text) - if match: - id = match.group() - if (id): - obj['acestream_id'] = id - results.append(obj) - return results \ No newline at end of file diff --git a/script.areace/script.py b/script.areace/script.py deleted file mode 100644 index b09afbe7b..000000000 --- a/script.areace/script.py +++ /dev/null @@ -1,7 +0,0 @@ -from resources.main_dialog import main_dialog - -def main(): - main_dialog() - -if (__name__ == '__main__'): - main() \ No newline at end of file diff --git a/script.common.plugin.cache/LICENSES/GPL-3.0-only.txt b/script.common.plugin.cache/LICENSES/GPL-3.0-only.txt deleted file mode 100644 index f288702d2..000000000 --- a/script.common.plugin.cache/LICENSES/GPL-3.0-only.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -them 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 prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey 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; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - 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. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -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. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/script.common.plugin.cache/addon.xml b/script.common.plugin.cache/addon.xml deleted file mode 100644 index 18ca5d794..000000000 --- a/script.common.plugin.cache/addon.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - -[fix] use xbmcvfs.translatePath when available, xbmc.translatePath deprecated(Matrix) and removed(Nexus) |contrib: Gujal00| - - - icon.png - - all - GPL-3.0-only - https://github.com/anxdpanic/script.common.plugin.cache - A common caching API for Kodi add-ons. - A common caching API for Kodi add-ons. - - diff --git a/script.common.plugin.cache/changelog.txt b/script.common.plugin.cache/changelog.txt deleted file mode 100644 index addb4054c..000000000 --- a/script.common.plugin.cache/changelog.txt +++ /dev/null @@ -1,75 +0,0 @@ -Version 2.6.2 -[fix] use xbmcvfs.translatePath when available, xbmc.translatePath deprecated(Matrix) and removed(Nexus) |contrib: Gujal00| - -[B]Version 2.6.1[/B] -[fix] shutdown taking longer than 5 seconds -[fix] ignore encoding issues for socket data - -[B]Version 2.6.0[/B] -- Kodi 19 / Python 3 ready -- convert strings.xml to strings.po -- use new language paths ie. ../resource.language.en_gb -- refactor -- housekeeping -- replace icon to resolve transparency issue - -[B]Version 2.5.10[/B] -- Fix self usage in non instance methods - -[B]Version 2.5.9[/B] -- Fix plugin loading on windows uwp -- Replace print with xbmc.log -- Replace except Exception , e with except Exception as e (python3) -- Replace iterkeys with keys (python3) - -[B]Version 2.5.8[/B] -- Fix plugin loading on windows - -[B]Version 2.5.7[/B] -- fixed platform check - -[B]Version 2.5.6[/B] -- fixed common cache not starting on ios/tvos when socket path is to long by using AF_INET instead of AF_POSIX - -[B]Version 2.5.5[/B] -- Fix ascii error with non UTF-8 characters on print statement - -[B]Version 1.5.2[/B] -- Fixed: Dharma error with sqlite. [mswiege] -- User selectable port on windows -- Notification of port/socket collision. - -[B]Version 1.5.1[/B] -- Fixed: Move position of socket and database file back - -[B]Version 1.5.0[/B] -- Changed: Move position of socket and database file - -[B]Version 1.4.0[/B] -- Fixed: edge case where unicode input values would cause cache to crash -- Fixed: Only bind to 127.0.0.1 on windows - -[B]Version 1.3.0[/B] -- Minor fixes - -[B]Version 1.2.0[/B] -- Minor fixes - -[B]Version 1.1.0[/B] -- Minor fixes - -[B]Version 1.0.0[/B] -- Minor fixes - -[B]Version 0.9.2[/B] -- Don't autodetect instancemode -- Set timeout of instancemode in settings - -[B]Version 0.9.1[/B] -- Update interface - -[B]Version 0.9.0[/B] -- Better stability - -[B]Version 0.8.0[/B] -- Initial public test run. diff --git a/script.common.plugin.cache/icon.png b/script.common.plugin.cache/icon.png deleted file mode 100644 index ea42296fd..000000000 Binary files a/script.common.plugin.cache/icon.png and /dev/null differ diff --git a/script.common.plugin.cache/resources/language/resource.language.en_gb/strings.po b/script.common.plugin.cache/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index 6c7311db5..000000000 --- a/script.common.plugin.cache/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -# XBMC Media Center language file -# Addon Name: Common plugin cache -# Addon id: script.common.plugin.cache -# Addon version: 2.6.0 -# Addon Provider: TheCollective -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2019-11-17 15:37+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "Addon Summary" -msgid "A common caching API for Kodi add-ons." -msgstr "" - -msgctxt "Addon Description" -msgid "A common caching API for Kodi add-ons." -msgstr "" - -msgctxt "#32001" -msgid "General" -msgstr "" - -msgctxt "#32002" -msgid "Debug" -msgstr "" - -msgctxt "#32003" -msgid "Start with Kodi" -msgstr "" - -msgctxt "#32004" -msgid "Timeout in seconds" -msgstr "" - -msgctxt "#32005" -msgid "Listen on Port (Windows only)" -msgstr "" - -msgctxt "#32100" -msgid "Error" -msgstr "" - -msgctxt "#32200" -msgid "Failed to start script.common.plugin.cache" -msgstr "" diff --git a/script.common.plugin.cache/resources/lib/__init__.py b/script.common.plugin.cache/resources/lib/__init__.py deleted file mode 100644 index 9608ed11a..000000000 --- a/script.common.plugin.cache/resources/lib/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2019 anxdpanic - - This file is part of script.common.plugin.cache - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only.txt for more information. -""" - -__all__ = ['entry_point', 'service', 'storage_server'] diff --git a/script.common.plugin.cache/resources/lib/entry_point.py b/script.common.plugin.cache/resources/lib/entry_point.py deleted file mode 100644 index cfd3459a3..000000000 --- a/script.common.plugin.cache/resources/lib/entry_point.py +++ /dev/null @@ -1,16 +0,0 @@ -""" - Cache service for Kodi - Version 0.8 - - Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen - Copyright (C) 2019 anxdpanic - - This file is part of script.common.plugin.cache - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only.txt for more information. -""" - -import service - -service.run() diff --git a/script.common.plugin.cache/resources/lib/service.py b/script.common.plugin.cache/resources/lib/service.py deleted file mode 100644 index 25474685c..000000000 --- a/script.common.plugin.cache/resources/lib/service.py +++ /dev/null @@ -1,68 +0,0 @@ -""" - Cache service for Kodi - Version 0.8 - - Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen - Copyright (C) 2019 anxdpanic - - This file is part of script.common.plugin.cache - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only.txt for more information. -""" - -import os -import sys -import threading - -import xbmc -import xbmcaddon -import xbmcgui -import xbmcvfs - -settings = xbmcaddon.Addon(id='script.common.plugin.cache') - - -def run(): - if settings.getSetting("autostart") == "true": - sleep_time = 10 - server_thread = None - - monitor = xbmc.Monitor() - while not monitor.abortRequested(): - if not server_thread: - server_thread = ServerThread() - - if monitor.waitForAbort(sleep_time): - break - - if server_thread: - server_thread.abort() - server_thread.join() - - -class ServerThread(threading.Thread): - def __init__(self): - super(ServerThread, self).__init__() - - self.server = None - - self.daemon = True - self.start() - - def run(self): - addon_path = settings.getAddonInfo('path') - if isinstance(addon_path, bytes): - addon_path = addon_path.decode('utf-8') - - sys.path = [os.path.join(addon_path, "resources", "lib", "storage_server")] + sys.path - - from storage_server import StorageServer - self.server = StorageServer.StorageServer(False) - - xbmc.log("[%s] Service loaded, starting server ..." % self.server.plugin, xbmc.LOGDEBUG) - - self.server.run() - - def abort(self): - self.server.force_abort = True diff --git a/script.common.plugin.cache/resources/lib/storage_server/StorageServer.py b/script.common.plugin.cache/resources/lib/storage_server/StorageServer.py deleted file mode 100644 index 9f6858395..000000000 --- a/script.common.plugin.cache/resources/lib/storage_server/StorageServer.py +++ /dev/null @@ -1,821 +0,0 @@ -""" - Cache service for Kodi - Version 0.8 - - Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen - Copyright (C) 2019 anxdpanic - - This file is part of script.common.plugin.cache - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only.txt for more information. -""" - -from contextlib import closing -import hashlib -import inspect -import os -import socket -import string -import sys -import time - -import xbmc - -sqlite3 = None -sqlite = None - -try: - import sqlite3 -except ImportError: - sqlite3 = None - try: - import sqlite - except ImportError: - sqlite = None - - -class StorageServer: - def __init__(self, table=None, timeout=24, instance=False): - if hasattr(sys.modules["__main__"], "xbmc"): - self.xbmc = sys.modules["__main__"].xbmc - else: - import xbmc - self.xbmc = xbmc - - if hasattr(sys.modules["__main__"], "xbmcvfs"): - self.xbmcvfs = sys.modules["__main__"].xbmcvfs - else: - import xbmcvfs - self.xbmcvfs = xbmcvfs - - if hasattr(sys.modules["__main__"], "xbmcaddon"): - self.xbmcaddon = sys.modules["__main__"].xbmcaddon - else: - import xbmcaddon - self.xbmcaddon = xbmcaddon - - if hasattr(sys.modules["__main__"], "xbmcgui"): - self.xbmcgui = sys.modules["__main__"].xbmcgui - else: - import xbmcgui - self.xbmcgui = xbmcgui - - if hasattr(self.xbmcvfs, "translatePath"): - self.translate_path = self.xbmcvfs.translatePath - else: - self.translate_path = self.xbmc.translatePath - - self.instance = instance - self._sock = None - self.die = False - self.force_abort = False - - self.settings = self.xbmcaddon.Addon(id='script.common.plugin.cache') - self.language = self.settings.getLocalizedString - - self.dbg = self.settings.getSetting("debug") == "true" - - self.version = to_unicode(self.settings.getAddonInfo('version')) - self.plugin = u"StorageClient-" + self.version - - self.path = to_unicode(self.translate_path('special://temp/')) - if not self.xbmcvfs.exists(self.path): - self._log(u"Making path structure: " + self.path) - self.xbmcvfs.mkdir(self.path) - self.path = os.path.join(self.path, 'commoncache.db') - - self.socket = "" - self.clientsocket = False - self.sql2 = True if sqlite else False - self.sql3 = True if sqlite3 else False - - self.daemon_start_time = time.time() - if self.instance: - self.idle = int(self.settings.getSetting("timeout")) - else: - self.idle = 3 - - self.platform = sys.platform - self.network_buffer_size = 4096 - - if isinstance(table, str) and len(table) > 0: - self.table = ''.join(c for c in table if c in "%s%s" % - (string.ascii_letters, string.digits)) - self._log("Setting table to : %s" % self.table) - elif table is False: - self._log("No table defined") - - self.timeout = float(timeout) * 3600 - - def _startDB(self): - try: - if self.sql3: - self._log("sql3 - " + self.path) - self.conn = sqlite3.connect(self.path, check_same_thread=False) - elif self.sql2: - self._log("sql2 - " + self.path) - self.conn = sqlite.connect(self.path) - else: - self._log("Error, no sql found") - return False - - self.curs = self.conn.cursor() - return True - except Exception as e: - self._log("Exception: " + repr(e)) - self.xbmcvfs.delete(self.path) - return False - - def _aborting(self): - if self.force_abort: - if self._sock: - self._sock.close() - return True - - if self.instance: - if self.die: - return True - else: - return self.xbmc.Monitor().abortRequested() - return False - - def _usePosixSockets(self): - if (self.platform in ["win32", 'win10'] or - xbmc.getCondVisibility('system.platform.android') or - xbmc.getCondVisibility('system.platform.ios') or - xbmc.getCondVisibility('system.platform.tvos')): - return False - else: - return True - - def _sock_init(self, check_stale=False): - if not self.socket or check_stale: - self._log("Checking") - - if self._usePosixSockets(): - self._log("POSIX") - self.socket = os.path.join(to_unicode(self.translate_path('special://temp/')), - 'commoncache.socket') - if self.xbmcvfs.exists(self.socket) and check_stale: - self._log("Deleting stale socket file : " + self.socket) - self.xbmcvfs.delete(self.socket) - else: - self._log("Non-POSIX") - port = self.settings.getSetting("port") - self.socket = ("127.0.0.1", int(port)) - - self._log("Done: " + repr(self.socket)) - - def _recieveData(self): - data = self._recv(self.clientsocket) - self._log("received data: " + data) - - try: - data = eval(data) - except: - self._log("Couldn't evaluate message : " + repr(data)) - data = {"action": "stop"} - - self._log("Done, got data: " + str(len(data)) + " - " + str(repr(data))[0:50]) - return data - - def _runCommand(self, data): - res = "" - if data["action"] == "get": - res = self._sqlGet(data["table"], data["name"]) - elif data["action"] == "get_multi": - res = self._sqlGetMulti(data["table"], data["name"], data["items"]) - elif data["action"] == "set_multi": - res = self._sqlSetMulti(data["table"], data["name"], data["data"]) - elif data["action"] == "set": - res = self._sqlSet(data["table"], data["name"], data["data"]) - elif data["action"] == "del": - res = self._sqlDel(data["table"], data["name"]) - elif data["action"] == "lock": - res = self._lock(data["table"], data["name"]) - elif data["action"] == "unlock": - res = self._unlock(data["table"], data["name"]) - - if len(res) > 0: - self._log("Got response: " + str(len(res)) + " - " + str(repr(res))[0:50]) - self._send(self.clientsocket, repr(res)) - - self._log("Done") - - def _showMessage(self, heading, message): - self._log(repr(type(heading)) + " - " + repr(type(message))) - icon = self.settings.getAddonInfo('icon') - self.xbmcgui.Dialog().notification(heading, message, icon, 10000, sound=False) - - def run(self): - self.plugin = "StorageServer-" + self.version - self._sock_init(True) - - if not self._startDB(): - self._startDB() - - if self._usePosixSockets(): - self._sock = socket.socket(socket.AF_UNIX) - else: - self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - with closing(self._sock) as open_socket: - try: - open_socket.bind(self.socket) - except Exception as e: - self._log("Exception: " + repr(e)) - self._showMessage(self.language(32100), self.language(32200)) - - return False - - open_socket.listen(1) - open_socket.setblocking(0) - - idle_since = time.time() - waiting = 0 - while not self._aborting(): - if waiting == 0: - self._log("accepting") - waiting = 1 - try: - (self.clientsocket, address) = open_socket.accept() - if waiting == 2: - self._log("Waking up, slept for %s seconds." % int(time.time() - idle_since)) - waiting = 0 - except socket.error as e: - if e.errno == 11 or e.errno == 10035 or e.errno == 35: - # There has to be a better way to accomplish this. - if idle_since + self.idle < time.time(): - if self.instance: - self.die = True - if waiting == 1: - self._log("Idle for %s seconds. Going to sleep. zzzzzzzz " % self.idle) - time.sleep(0.5) - waiting = 2 - continue - self._log("EXCEPTION : " + repr(e)) - except: - pass - - if waiting: - self._log("Continue : " + repr(waiting)) - continue - - data = self._recieveData() - self._runCommand(data) - idle_since = time.time() - - self._log("Done") - - self._log("Closing down") - # self.conn.close() - - if self._usePosixSockets(): - if self.xbmcvfs.exists(self.socket): - self._log("Deleting socket file") - self.xbmcvfs.delete(self.socket) - self.xbmc.log(self.plugin + " Closed down") - - def _recv(self, sock): - data = " " - idle = True - - i = 0 - start = time.time() - recv_buffer = "" - while data[len(data) - 2:] != "\r\n" or not idle: - try: - if idle: - recv_buffer = sock.recv(self.network_buffer_size) - idle = False - i += 1 - recv_buffer = recv_buffer.decode('utf-8', 'ignore') - self._log(u"got data : " + str(i) + u" - " + repr(idle) + u" - " + - str(len(data)) + u" + " + str(len(recv_buffer)) + u" | " + - repr(recv_buffer)[len(recv_buffer) - 5:]) - data += recv_buffer - start = time.time() - elif not idle: - if data[len(data) - 2:] == "\r\n": - content = "COMPLETE\r\n" + (" " * (15 - len("COMPLETE\r\n"))) - content = content.encode('utf-8', 'ignore') - sock.send(content) - idle = True - self._log(u"sent COMPLETE " + str(i)) - elif len(recv_buffer) > 0: - content = "ACK\r\n" + (" " * (15 - len("ACK\r\n"))) - content = content.encode('utf-8', 'ignore') - sock.send(content) - idle = True - self._log(u"sent ACK " + str(i)) - self._log(u"status " + repr(not idle) + u" - " + - repr(data[len(data) - 2:] != u"\r\n")) - - except socket.error as e: - if e.errno not in [10035, 35]: - self._log(u"Except error " + repr(e)) - - if e.errno in [22]: # We can't fix this. - return "" - - if start + 10 < time.time(): - self._log(u"over time") - break - - self._log(u"done") - return data.strip() - - def _send(self, sock, data): - idle = True - status = "" - result = "" - self._log(str(len(data)) + u" - " + repr(data)[0:20]) - i = 0 - start = time.time() - while len(data) > 0 or not idle: - send_buffer = " " - try: - if idle: - if len(data) > self.network_buffer_size: - send_buffer = data[:self.network_buffer_size] - else: - send_buffer = data + "\r\n" - send_buffer = send_buffer.encode('utf-8', 'ignore') - result = sock.send(send_buffer) - i += 1 - idle = False - start = time.time() - elif not idle: - status = "" - while status.find("COMPLETE\r\n") == -1 and status.find("ACK\r\n") == -1: - status = sock.recv(15) - status = status.decode('utf-8', 'ignore') - i -= 1 - - idle = True - if len(data) > self.network_buffer_size: - data = data[self.network_buffer_size:] - else: - data = "" - - self._log(u"Got response " + str(i) + u" - " + str(result) + u" == " + - str(len(send_buffer)) + u" | " + str(len(data)) + u" - " + - repr(send_buffer)[len(send_buffer) - 5:]) - - except socket.error as e: - self._log(u"Except error " + repr(e)) - if e.errno != 10035 and e.errno != 35 and e.errno != 107 and e.errno != 32: - self._log(u"Except error " + repr(e)) - if start + 10 < time.time(): - self._log(u"Over time") - break - self._log(u"Done") - return status.find(u"COMPLETE\r\n") > -1 - - def _lock(self, table, name): # This is NOT atomic - self._log(name) - locked = True - curlock = self._sqlGet(table, name) - if curlock.strip(): - if float(curlock) < self.daemon_start_time: - self._log(u"removing stale lock.") - self._sqlExecute("DELETE FROM " + table + " WHERE name = %s", (name,)) - self.conn.commit() - locked = False - else: - locked = False - - if not locked: - self._sqlExecute("INSERT INTO " + table + " VALUES ( %s , %s )", (name, time.time())) - self.conn.commit() - self._log(u"locked: " + to_unicode(name)) - - return "true" - - self._log(u"failed for : " + to_unicode(name)) - return "false" - - def _unlock(self, table, name): - self._log(name) - - self._checkTable(table) - self._sqlExecute("DELETE FROM " + table + " WHERE name = %s", (name,)) - - self.conn.commit() - self._log(u"done") - return "true" - - def _sqlSetMulti(self, table, pre, inp_data): - self._log(pre) - self._checkTable(table) - for name in inp_data: - if self._sqlGet(table, pre + name).strip(): - self._log(u"Update : " + pre + to_unicode(name)) - self._sqlExecute("UPDATE " + table + " SET data = %s WHERE name = %s", - (inp_data[name], pre + name)) - else: - self._log(u"Insert : " + pre + to_unicode(name)) - self._sqlExecute("INSERT INTO " + table + " VALUES ( %s , %s )", - (pre + name, inp_data[name])) - - self.conn.commit() - self._log(u"Done") - return "" - - def _sqlGetMulti(self, table, pre, items): - self._log(pre) - - self._checkTable(table) - ret_val = [] - for name in items: - self._log(pre + name) - self._sqlExecute("SELECT data FROM " + table + " WHERE name = %s", (pre + name)) - - result = "" - for row in self.curs: - self._log(u"Adding : " + str(repr(row[0]))[0:20]) - result = row[0] - ret_val += [result] - - self._log(u"Returning : " + repr(ret_val)) - return ret_val - - def _sqlSet(self, table, name, data): - self._log(name + str(repr(data))[0:20]) - - self._checkTable(table) - if self._sqlGet(table, name).strip(): - self._log(u"Update : " + to_unicode(data)) - self._sqlExecute("UPDATE " + table + " SET data = %s WHERE name = %s", (data, name)) - else: - self._log(u"Insert : " + to_unicode(data)) - self._sqlExecute("INSERT INTO " + table + " VALUES ( %s , %s )", (name, data)) - - self.conn.commit() - self._log(u"Done") - return "" - - def _sqlDel(self, table, name): - self._log(name + u" - " + table) - - self._checkTable(table) - - self._sqlExecute("DELETE FROM " + table + " WHERE name LIKE %s", name) - self.conn.commit() - self._log(u"done") - return "true" - - def _sqlGet(self, table, name): - self._log(name + u" - " + table) - - self._checkTable(table) - self._sqlExecute("SELECT data FROM " + table + " WHERE name = %s", name) - - for row in self.curs: - self._log(u"Returning : " + str(repr(row[0]))[0:20]) - return row[0] - - self._log(u"Returning empty") - return " " - - def _sqlExecute(self, sql, data): - try: - self._log(repr(sql) + u" - " + repr(data)) - if self.sql2: - self.curs.execute(sql, data) - elif self.sql3: - sql = sql.replace("%s", "?") - if isinstance(data, tuple): - self.curs.execute(sql, data) - else: - self.curs.execute(sql, (data,)) - except sqlite3.DatabaseError as e: - if (self.xbmcvfs.exists(self.path) and - (str(e).find("file is encrypted") > -1 or str(e).find("not a database") > -1)): - self._log(u"Deleting broken database file") - self.xbmcvfs.delete(self.path) - self._startDB() - else: - self._log(u"Database error, but database NOT deleted: " + repr(e)) - except: - self._log(u"Uncaught exception") - - def _checkTable(self, table): - try: - self.curs.execute("create table " + table + " (name text unique, data text)") - self.conn.commit() - self._log(u"Created new table") - except: - self._log(u"Passed") - pass - - def _evaluate(self, data): - try: - data = eval(data) # Test json.loads vs eval - return data - except: - self._log(u"Couldn't evaluate message : " + repr(data)) - return "" - - def _generateKey(self, funct, *args): - name = repr(funct) - if name.find(" of ") > -1: - name = name[name.find("method") + 7:name.find(" of ")] - elif name.find(" at ") > -1: - name = name[name.find("function") + 9:name.find(" at ")] - - keyhash = hashlib.md5() - for params in args: - if isinstance(params, dict): - for key in sorted(params.keys()): - if key not in ["new_results_function"]: - val = params[key] - if not isinstance(val, str): - val = str(val) - if isinstance(key, str): - key = key.encode('utf-8') - if isinstance(val, str): - val = val.encode('utf-8') - key_val_pair = b"'%s'='%s'" % (key, val) - keyhash.update(key_val_pair) - elif isinstance(params, list): - hash_list = [] - for el in params: - if not isinstance(el, str): - el = str(el) - if isinstance(el, str): - el = el.encode('utf-8') - hash_list.append(el) - keyhash.update(b",".join([b"%s" % el for el in hash_list])) - else: - if not isinstance(params, str): - params = str(params) - if isinstance(params, str): - params = params.encode('utf-8') - keyhash.update(params) - - name += "|" + keyhash.hexdigest() + "|" - - self._log(u"Done: " + repr(name)) - return name - - def _getCache(self, name, cache): - if name in cache: - if "timeout" not in cache[name]: - cache[name]["timeout"] = 3600 - - if cache[name]["timestamp"] > time.time() - (cache[name]["timeout"]): - self._log(u"Done, found cache : " + to_unicode(name)) - return cache[name]["res"] - else: - self._log(u"Deleting old cache : " + to_unicode(name)) - del (cache[name]) - - self._log(u"Done") - return False - - def _setCache(self, cache, name, ret_val): - if len(ret_val) > 0: - if not isinstance(cache, dict): - cache = {} - cache[name] = {"timestamp": time.time(), - "timeout": self.timeout, - "res": ret_val} - self._log(u"Saving cache: " + name + str(repr(cache[name]["res"]))[0:50]) - self.set("cache" + name, repr(cache)) - self._log(u"Done") - return ret_val - - # EXTERNAL FUNCTIONS - soccon = False - table = False - - def cacheFunction(self, funct=False, *args): - self._log(u"function : " + repr(funct) + u" - table_name: " + repr(self.table)) - if funct and self.table: - name = self._generateKey(funct, *args) - cache = self.get("cache" + name) - - if cache.strip() == "": - cache = {} - else: - cache = self._evaluate(cache) - - ret_val = self._getCache(name, cache) - - if not ret_val: - self._log(u"Running: " + to_unicode(name)) - ret_val = funct(*args) - self._setCache(cache, name, ret_val) - - if ret_val: - self._log(u"Returning result: " + str(len(ret_val))) - self._log(ret_val) - return ret_val - else: - self._log(u"Returning []. Got result: " + repr(ret_val)) - return [] - - self._log(u"Error") - return [] - - def cacheDelete(self, name): - self._log(name) - if self._connect() and self.table: - temp = repr({"action": "del", "table": self.table, "name": "cache" + name}) - self._send(self.soccon, temp) - res = self._recv(self.soccon) - self._log(u"GOT " + repr(res)) - - def cacheClean(self, empty=False): - if self.table: - cache = self.get("cache" + self.table) - - try: - cache = self._evaluate(cache) - except: - self._log(u"Couldn't evaluate message : " + repr(cache)) - - self._log(u"Cache : " + repr(cache)) - if cache: - new_cache = {} - for item in cache: - if (cache[item]["timestamp"] > (time.time() - 3600)) and not empty: - new_cache[item] = cache[item] - else: - self._log(u"Deleting: " + to_unicode(item)) - - self.set("cache", repr(new_cache)) - return True - - return False - - def lock(self, name): - self._log(name) - self._log(self.table) - - if self._connect() and self.table: - data = repr({"action": "lock", "table": self.table, "name": name}) - self._send(self.soccon, data) - res = self._recv(self.soccon) - if res: - res = self._evaluate(res) - - if res == "true": - self._log(u"Done : " + res.strip()) - return True - - self._log(u"Failed") - return False - - def unlock(self, name): - self._log(name) - - if self._connect() and self.table: - data = repr({"action": "unlock", "table": self.table, "name": name}) - self._send(self.soccon, data) - res = self._recv(self.soccon) - if res: - res = self._evaluate(res) - - if res == "true": - self._log(u"Done: " + res.strip()) - return True - - self._log(u"Failed") - return False - - def _connect(self): - self._sock_init() - - if self._usePosixSockets(): - self.soccon = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - else: - self.soccon = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - connected = False - try: - self.soccon.connect(self.socket) - connected = True - except socket.error as e: - if e.errno in [111]: - self._log(u"StorageServer isn't running") - else: - self._log(u"Exception: " + repr(e)) - self._log(u"Exception: " + repr(self.socket)) - - return connected - - def setMulti(self, name, data): - self._log(name) - if self._connect() and self.table: - temp = repr({"action": "set_multi", "table": self.table, "name": name, "data": data}) - res = self._send(self.soccon, temp) - self._log(u"GOT " + repr(res)) - - def getMulti(self, name, items): - self._log(name) - if self._connect() and self.table: - self._send(self.soccon, repr( - { - "action": "get_multi", - "table": self.table, - "name": name, - "items": items - } - )) - self._log(u"Receive") - res = self._recv(self.soccon) - - self._log(u"res : " + str(len(res))) - if res: - res = self._evaluate(res) - - if res == " ": # We return " " as nothing. - return "" - else: - return res - - return "" - - def delete(self, name): - self._log(name) - if self._connect() and self.table: - temp = repr({"action": "del", "table": self.table, "name": name}) - self._send(self.soccon, temp) - res = self._recv(self.soccon) - self._log(u"GOT " + repr(res)) - - def set(self, name, data): - self._log(name) - if self._connect() and self.table: - temp = repr({"action": "set", "table": self.table, "name": name, "data": data}) - res = self._send(self.soccon, temp) - self._log(u"GOT " + repr(res)) - - def get(self, name): - self._log(name) - if self._connect() and self.table: - self._send(self.soccon, repr({"action": "get", "table": self.table, "name": name})) - self._log(u"Receive") - res = self._recv(self.soccon) - - self._log(u"res : " + str(len(res))) - if res: - res = self._evaluate(res) - return res.strip() # We return " " as nothing. Strip it out. - - return "" - - def setCacheTimeout(self, timeout): - self.timeout = float(timeout) * 3600 - - def _log(self, description): - if self.dbg: - try: - self.xbmc.log(u"[%s] %s : '%s'" % - (self.plugin, repr(inspect.stack()[1][3]), description), - self.xbmc.LOGDEBUG) - except: - self.xbmc.log(u"[%s] %s : '%s'" % - (self.plugin, repr(inspect.stack()[1][3]), - repr(description)), self.xbmc.LOGDEBUG) - - -def to_unicode(text): - if isinstance(text, bytes): - return text.decode('utf-8') - return text - - -# Check if this module should be run in instance mode or not. -__workersByName = {} - - -def run_async(func, *args, **kwargs): - from threading import Thread - worker = Thread(target=func, args=args, kwargs=kwargs) - __workersByName[worker.getName()] = worker - worker.start() - return worker - - -def checkInstanceMode(): - if hasattr(sys.modules["__main__"], "xbmcaddon"): - xbmcaddon = sys.modules["__main__"].xbmcaddon - else: - import xbmcaddon - - settings = xbmcaddon.Addon(id='script.common.plugin.cache') - if settings.getSetting("autostart") == "false": - s = StorageServer(table=False, instance=True) - xbmc.log("[%s] Module loaded (instance only), starting server ..." % s.plugin, - xbmc.LOGDEBUG) - run_async(s.run) - return True - else: - return False - - -_ = checkInstanceMode() diff --git a/script.common.plugin.cache/resources/lib/storage_server/__init__.py b/script.common.plugin.cache/resources/lib/storage_server/__init__.py deleted file mode 100644 index 38cc3650b..000000000 --- a/script.common.plugin.cache/resources/lib/storage_server/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2019 anxdpanic - - This file is part of script.common.plugin.cache - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only.txt for more information. -""" - -__all__ = ['StorageServer', 'storageserverdummy'] diff --git a/script.common.plugin.cache/resources/lib/storage_server/storageserverdummy.py b/script.common.plugin.cache/resources/lib/storage_server/storageserverdummy.py deleted file mode 100644 index 77c5818ec..000000000 --- a/script.common.plugin.cache/resources/lib/storage_server/storageserverdummy.py +++ /dev/null @@ -1,40 +0,0 @@ -""" - StorageServer override - Version: 1.0 - - Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen - Copyright (C) 2019 anxdpanic - - This file is part of script.common.plugin.cache - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only.txt for more information. -""" - - -class StorageServer: - def __init__(self, table, timeout=24): - pass - - def cacheFunction(self, funct=False, *args): - if funct: - return funct(*args) - return [] - - def set(self, name, data): - return "" - - def get(self, name): - return "" - - def setMulti(self, name, data): - return "" - - def getMulti(self, name, items): - return "" - - def lock(self, name): - return False - - def unlock(self, name): - return False diff --git a/script.common.plugin.cache/resources/settings.xml b/script.common.plugin.cache/resources/settings.xml deleted file mode 100644 index 6865ba1f8..000000000 --- a/script.common.plugin.cache/resources/settings.xml +++ /dev/null @@ -1,38 +0,0 @@ - - -
- - - - 0 - false - - - - 0 - true - - - - 0 - 59994 - - 32005 - - - - 0 - 15 - - - true - - - - 32004 - - - - -
-
diff --git a/script.copacetic.helper/LICENSE.txt b/script.copacetic.helper/LICENSE.txt deleted file mode 100644 index 61d18602b..000000000 --- a/script.copacetic.helper/LICENSE.txt +++ /dev/null @@ -1,674 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -them 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 prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey 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; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - 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. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -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. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. \ No newline at end of file diff --git a/script.copacetic.helper/README.md b/script.copacetic.helper/README.md deleted file mode 100644 index e9fac0fa1..000000000 --- a/script.copacetic.helper/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Copacetic Helper [![License](https://img.shields.io/badge/License-GPLv3-blue)](https://github.com/realcopacetic/script.copacetic.helper/blob/main/LICENSE.txt) - -All code contained in this project is licensed under GPL 3.0. - -### Credit ---- -* __sualfred__ for [script.embuary.helper](https://github.com/sualfred/script.embuary.helper), which served as the basis for several parts of this addon. The script and service sections are completely rewritten/modified, but some of the code and structure of the plugin section remains a very simplified version of Sualfred's work. Credit included in each file where code remains. - -* __Dodi Achmad on Unsplash__ for image used in addon [fanart.jpg](https://unsplash.com/photos/3qaojaP-6cE). Free for use under Unsplash licence. - -### Special thanks ---- -* __jurialmunkey__ for all the best-practice code examples from [plugin.video.themoviedb.helper](https://github.com/jurialmunkey/plugin.video.themoviedb.helper) and forum support. - -### Changelog - -**1.0.8** -- Push dbid of corresponding cropped clearlogo to window prop for comparison so cropped clearlogos only show on correct listitems. - -**1.0.7** -- Add tvguide to SlideShowMonitor() whitelist - -**1.0.6** -- Moved director/writer/studio/genre splitting to monitoring service - -**1.0.5** -- Added script for easily re-ordering widgets in Copacetic settings screen - -**1.0.4** -- Return dominant colour for home widgets when clearlogo cropper is active -- Added fanart multiart to backgtround slideshows - -**1.0.3** -- Updated fanart. - -**1.0.2** -- Fixes for errors flagged by Kodi Addon Checker workflow during submission process. - -**1.0.1** -- Fix for an error when the label passessd to the function clean_filename() was not escaped properly. Now to avoid the issue, by default, if no label is provided, the function will pull the listitem label directly using xbmc.getInfoLabel('ListItem.Label') - -**1.0.0** -- Initial release. \ No newline at end of file diff --git a/script.copacetic.helper/addon.xml b/script.copacetic.helper/addon.xml deleted file mode 100644 index 5af3e408f..000000000 --- a/script.copacetic.helper/addon.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - video - - - executable - - - - all - GPL-3.0-or-later - - resources/icon.png - resources/fanart.jpg - - Extend the power of Copacetic - Helper script for the skin Copacetic. - - diff --git a/script.copacetic.helper/plugin.py b/script.copacetic.helper/plugin.py deleted file mode 100644 index 6a94d4679..000000000 --- a/script.copacetic.helper/plugin.py +++ /dev/null @@ -1,6 +0,0 @@ -# author: realcopacetic, sualfred - -from resources.lib.plugin.main import Main - -if __name__ == '__main__': - Main() diff --git a/script.copacetic.helper/resources/fanart.jpg b/script.copacetic.helper/resources/fanart.jpg deleted file mode 100644 index 87cb05656..000000000 Binary files a/script.copacetic.helper/resources/fanart.jpg and /dev/null differ diff --git a/script.copacetic.helper/resources/icon.png b/script.copacetic.helper/resources/icon.png deleted file mode 100644 index 90863a45e..000000000 Binary files a/script.copacetic.helper/resources/icon.png and /dev/null differ diff --git a/script.copacetic.helper/resources/language/resource.language.en_gb/strings.po b/script.copacetic.helper/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index 381d905f6..000000000 --- a/script.copacetic.helper/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,114 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Copacetic Helper -# Addon id: script.copacetic.helper -# Addon Provider: realcopacetic -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - - - - -######################################################################################################### -#: Contents - -#: SETTINGS 320XX -#: SERVICE 322XX -#: SCRIPT 324XX -#: PLUGIN 326XX - - - - -######################################################################################################### - -#: SETTINGS 320XX - -######################################################################################################### - -#: /resources/settings.xml -msgctxt "#32000" -msgid "Copacetic" -msgstr "" - -#: /resources/settings.xml -msgctxt "#32001" -msgid "General" -msgstr "" - -#: /resources/settings.xml -msgctxt "#32002" -msgid "Debug" -msgstr "" - -#: /resources/settings.xml -msgctxt "#32003" -msgid "Enable logging" -msgstr "" - -#: /resources/settings.xml -msgctxt "#32004" -msgid "- Include json call/response logging" -msgstr "" - - - - -######################################################################################################### - -#: SERVICE 322XX - -######################################################################################################### - -#: /resources/settings.xml -msgctxt "#32201" -msgid "Cropped cache cleared" -msgstr "" - -#: /resources/settings.xml -msgctxt "#32202" -msgid "saved" -msgstr "" - -#: /resources/settings.xml -msgctxt "#32203" -msgid "setting changed" -msgstr "" - -#: /resources/settings.xml -msgctxt "#32204" -msgid "settings changed" -msgstr "" - - - - -######################################################################################################### - -#: PLUGIN 326XX - -######################################################################################################### - -#: /resources/lib/content.py -msgctxt "#32600" -msgid "Next up" -msgstr "" - -#: /resources/lib/content.py -msgctxt "#32601" -msgid "Continue watching" -msgstr "" - -#: /resources/lib/content.py -msgctxt "#32602" -msgid "Director credits" -msgstr "" - -#: /resources/lib/content.py -msgctxt "#32603" -msgid "Actor credits" -msgstr "" diff --git a/script.copacetic.helper/resources/lib/plugin/content.py b/script.copacetic.helper/resources/lib/plugin/content.py deleted file mode 100644 index d513dc1da..000000000 --- a/script.copacetic.helper/resources/lib/plugin/content.py +++ /dev/null @@ -1,269 +0,0 @@ -# author: realcopacetic, sualfred - -from resources.lib.plugin.json_map import JSON_MAP -from resources.lib.plugin.library import * -from resources.lib.utilities import (ADDON, infolabel, json_call, log, - set_plugincontent) - - -class PluginContent(object): - def __init__(self, params, li): - self.dbtitle = params.get('title') - self.dbtype = params.get('type') - self.limit = params.get('limit') - self.label = params.get('label') - self.exclude_key = params.get('exclude_key') - self.exclude_value = params.get('exclude_value') - self.li = li - - if not self.exclude_key: - self.exclude_key = 'title' - - if self.limit: - self.limit = int(self.limit) - - if self.dbtype: - if self.dbtype in ['movie', 'tvshow', 'season', 'episode', 'musicvideo']: - library = 'Video' - else: - library = 'Audio' - - self.method_details = f'{library}Library.Get{self.dbtype}Details' - self.method_item = f'{library}Library.Get{self.dbtype}s' - self.param = f'{self.dbtype}id' - self.key_details = f'{self.dbtype}details' - self.key_items = f'{self.dbtype}s' - self.properties = JSON_MAP.get(f'{self.dbtype}_properties') - - self.sort_lastplayed = {'order': 'descending', 'method': 'lastplayed'} - self.sort_recent = {'order': 'descending', 'method': 'dateadded'} - self.sort_year = {'order': 'descending', 'method': 'year'} - self.sort_random = {'method': 'random'} - - self.filter_unwatched = {'field': 'playcount', - 'operator': 'lessthan', 'value': '1'} - self.filter_watched = {'field': 'playcount', - 'operator': 'greaterthan', 'value': '0'} - self.filter_unwatched_episodes = { - 'field': 'numwatched', 'operator': 'lessthan', 'value': ['1']} - self.filter_watched_episodes = { - 'field': 'numwatched', 'operator': 'greaterthan', 'value': ['0']} - self.filter_no_specials = {'field': 'season', - 'operator': 'greaterthan', 'value': '0'} - self.filter_inprogress = { - 'field': 'inprogress', 'operator': 'true', 'value': ''} - self.filter_not_inprogress = { - 'field': 'inprogress', 'operator': 'false', 'value': ''} - self.filter_title = {'field': 'title', - 'operator': 'is', 'value': self.dbtitle} - self.filter_director = {'field': 'director', - 'operator': 'is', 'value': self.label} - self.filter_actor = {'field': 'actor', - 'operator': 'is', 'value': self.label} - if self.exclude_value: - self.filter_exclude = {'field': self.exclude_key, - 'operator': 'isnot', 'value': self.exclude_value} - - def in_progress(self): - filters = [self.filter_inprogress] - - if self.dbtype != 'tvshow': - json_query = json_call('VideoLibrary.GetMovies', - properties=JSON_MAP['movie_properties'], - sort=self.sort_lastplayed, - query_filter={'and': filters}, - parent='in_progress' - ) - try: - json_query = json_query['result']['movies'] - except Exception: - log('Widget in_progress: No movies found.') - else: - add_items(self.li, json_query, type='movie') - - if self.dbtype != 'movie': - json_query = json_call('VideoLibrary.GetEpisodes', - properties=JSON_MAP['episode_properties'], - sort=self.sort_lastplayed, - query_filter={'and': filters}, - parent='in_progress' - ) - try: - json_query = json_query['result']['episodes'] - except Exception: - log('Widget in_progress: No episodes found.') - else: - for episode in json_query: - tvshowid = episode.get('tvshowid') - tvshow_json_query = json_call( - 'VideoLibrary.GetTVShowDetails', - params={'tvshowid': tvshowid}, - properties=['studio', 'mpaa'], - parent='in_progress' - ) - try: - tvshow_json_query = tvshow_json_query['result']['tvshowdetails'] - except Exception: - log(f'Widget in_progress: Parent tv show not found --> {tvshowid}') - else: - episode['studio'] = tvshow_json_query.get('studio') - episode['mpaa'] = tvshow_json_query.get('mpaa') - add_items(self.li, json_query, type='episode') - set_plugincontent(content='movies', - category=ADDON.getLocalizedString(32601)) - - def next_up(self): - filters = [self.filter_inprogress] - - json_query = json_call('VideoLibrary.GetTVShows', - properties=['title', 'lastplayed', - 'studio', 'mpaa'], - sort=self.sort_lastplayed, limit=25, - query_filter={'and': filters}, - parent='next_up' - ) - - try: - json_query = json_query['result']['tvshows'] - except Exception: - log('Widget next_up: No TV shows found') - return - - for episode in json_query: - use_last_played_season = True - studio = episode.get('studio', '') - mpaa = episode.get('mpaa', '') - last_played_query = json_call('VideoLibrary.GetEpisodes', - properties=['seasonid', 'season'], - sort={'order': 'descending', 'method': 'lastplayed'}, limit=1, - query_filter={'and': [ - {'or': [self.filter_inprogress, self.filter_watched]}, self.filter_no_specials]}, - params={'tvshowid': int( - episode['tvshowid'])}, - parent='next_up' - ) - - if last_played_query['result']['limits']['total'] < 1: - use_last_played_season = False - - ''' Return the next episode of last played season''' - if use_last_played_season: - episode_query = json_call('VideoLibrary.GetEpisodes', - properties=JSON_MAP['episode_properties'], - sort={'order': 'ascending', 'method': 'episode'}, limit=1, - query_filter={'and': [self.filter_unwatched, {'field': 'season', 'operator': 'is', 'value': str( - last_played_query['result']['episodes'][0].get('season'))}]}, - params={'tvshowid': int( - episode['tvshowid'])}, - parent='next_up' - ) - - if episode_query['result']['limits']['total'] < 1: - use_last_played_season = False - - ''' If no episode is left of the last played season, fall back to the very first unwatched episode''' - if not use_last_played_season: - episode_query = json_call('VideoLibrary.GetEpisodes', - properties=JSON_MAP['episode_properties'], - sort={'order': 'ascending', 'method': 'episode'}, limit=1, - query_filter={ - 'and': [self.filter_unwatched, self.filter_no_specials]}, - params={'tvshowid': int( - episode['tvshowid'])}, - parent='next_up' - ) - - try: - episode_details = episode_query['result']['episodes'] - except Exception: - log( - f"Widget next_up: No next episodes found for {episode['title']}") - else: - ''' Add tv show studio and mpaa to episode dictionary ''' - episode_details[0]['studio'] = studio - episode_details[0]['mpaa'] = mpaa - add_items(self.li, episode_details, type='episode') - set_plugincontent(content='episodes', - category=ADDON.getLocalizedString(32600)) - - def director_credits(self): - filters = [self.filter_director] - if self.filter_exclude: - filters.append(self.filter_exclude) - - json_query = json_call('VideoLibrary.GetMovies', - properties=JSON_MAP['movie_properties'], - sort=self.sort_year, - query_filter={'and': filters}, - parent='director_credits' - ) - - try: - json_query = json_query['result']['movies'] - except Exception: - log('Widget director_credits: No movies found.') - else: - add_items(self.li, json_query, type='movie') - - json_query = json_call('VideoLibrary.GetMusicVideos', - properties=JSON_MAP['musicvideo_properties'], - sort=self.sort_year, - query_filter={'and': filters}, - parent='director_credits' - ) - - try: - json_query = json_query['result']['musicvideos'] - except Exception: - log('Widget director_credits: No music videos found.') - else: - add_items(self.li, json_query, type='musicvideo') - - set_plugincontent(content='videos', - category=ADDON.getLocalizedString(32602)) - - def actor_credits(self): - filters = [self.filter_actor] - current_item = infolabel('ListItem.Label') - - movies_json_query = json_call('VideoLibrary.GetMovies', - properties=JSON_MAP['movie_properties'], - sort=self.sort_year, - query_filter={'and': filters}, - parent='actor_credits' - ) - - tvshows_json_query = json_call('VideoLibrary.GetTVShows', - properties=JSON_MAP['tvshow_properties'], - sort=self.sort_year, - query_filter={'and': filters}, - parent='actor_credits' - ) - - total_items = int(movies_json_query['result']['limits']['total']) + int( - tvshows_json_query['result']['limits']['total']) - - try: - movies_json_query = movies_json_query['result']['movies'] - except Exception: - log(f'Widget actor_credits: No movies found for {self.label}.') - else: - dict_to_remove = next( - (item for item in movies_json_query if item['label'] == current_item), None) - movies_json_query.remove( - dict_to_remove) if dict_to_remove is not None and total_items > 1 else None - add_items(self.li, movies_json_query, type='movie') - - try: - tvshows_json_query = tvshows_json_query['result']['tvshows'] - except Exception: - log(f'Widget actor_credits: No tv shows found for {self.label}.') - else: - dict_to_remove = next( - (item for item in tvshows_json_query if item['label'] == current_item), None) - tvshows_json_query.remove( - dict_to_remove) if dict_to_remove is not None and total_items > 1 else None - add_items(self.li, tvshows_json_query, type='tvshow') - - set_plugincontent(content='videos', - category=ADDON.getLocalizedString(32603)) diff --git a/script.copacetic.helper/resources/lib/plugin/json_map.py b/script.copacetic.helper/resources/lib/plugin/json_map.py deleted file mode 100644 index df11cc8b7..000000000 --- a/script.copacetic.helper/resources/lib/plugin/json_map.py +++ /dev/null @@ -1,256 +0,0 @@ -# author: realcopacetic, sualfred - -JSON_MAP = { - 'movie_properties': [ - "title", - #"genre", - "year", - #"rating", - #"director", - "trailer", - #"tagline", - #"plot", - #"plotoutline", - #"originaltitle", - "lastplayed", - "playcount", - #"writer", - "studio", - "mpaa", - #"cast", - #"country", - #"imdbnumber", - "runtime", - #"set", - #"showlink", - "streamdetails", - #"top250", - #"votes", - #"fanart", - #"thumbnail", - "file", - #"sorttitle", - "resume", - #"setid", - #"dateadded", - #"tag", - "art" - #"userrating", - #"ratings", - #"premiered", - #"uniqueid" - ], - - 'episode_properties': [ - "title", - #"plot", - #"votes", - #"rating", - #"writer", - "firstaired", - "playcount", - "runtime", - #"director", - #"productioncode", - "season", - "episode", - #"originaltitle", - "showtitle", - #"cast", - "streamdetails", - "lastplayed", - #"fanart", - #"thumbnail", - "file", - "resume", - "tvshowid", - #"dateadded", - #"uniqueid", - "art" - #"specialsortseason", - #"specialsortepisode", - #"userrating", - #"seasonid", - #"ratings" - ], - - 'tvshow_properties': [ - "title", - #"genre", - "year", - #"rating", - #"plot", - "studio", - "mpaa", - #"cast", - #"playcount", - "episode", - #"imdbnumber", - #"premiered", - #"votes", - "lastplayed", - #"fanart", - #"thumbnail", - "file", - #"originaltitle", - #"sorttitle", - #"episodeguide", - "season", - "watchedepisodes", - #"dateadded", - #"tag", - "art" - #"userrating", - #"ratings", - #"runtime", - #"uniqueid" - ], - - 'musicvideo_properties': [ - "title", - "playcount", - "runtime", - #"director", - "studio", - "year", - #"plot", - #"album", - "artist", - #"genre", - #"track", - "streamdetails", - "lastplayed", - #"fanart", - #"thumbnail", - "file", - "resume", - #"dateadded", - #"tag", - "art" - #"rating", - #"userrating", - #"premiered" - ], - - 'season_properties': [ - "season", - "showtitle", - "playcount", - "episode", - "fanart", - "thumbnail", - "tvshowid", - "watchedepisodes", - "art", - "userrating", - "title" - ], - - 'artist_properties': [ - 'instrument', - 'style', - 'mood', - 'born', - 'formed', - 'description', - 'genre', - 'died', - 'disbanded', - 'yearsactive', - 'musicbrainzartistid', - 'fanart', - 'thumbnail', - 'compilationartist', - 'dateadded', - 'roles', - 'songgenres', - 'isalbumartist' - ], - - 'playlist_properties': [ - 'title', - 'artist', - 'albumartist', - 'genre', - 'year', - 'rating', - 'album', - 'track', - 'duration', - 'comment', - 'lyrics', - 'musicbrainztrackid', - 'musicbrainzartistid', - 'musicbrainzalbumid', - 'musicbrainzalbumartistid', - 'playcount', - 'fanart', - 'director', - 'trailer', - 'tagline', - 'plot', - 'plotoutline', - 'originaltitle', - 'lastplayed', - 'writer', - 'studio', - 'mpaa', - 'cast', - 'country', - 'imdbnumber', - 'premiered', - 'productioncode', - 'runtime', - 'set', - 'showlink', - 'streamdetails', - 'top250', - 'votes', - 'firstaired', - 'season', - 'episode', - 'showtitle', - 'thumbnail', - 'file', - 'resume', - 'artistid', - 'albumid', - 'tvshowid', - 'setid', - 'watchedepisodes', - 'disc', - 'tag', - 'art', - 'genreid', - 'displayartist', - 'albumartistid', - 'description', - 'theme', - 'mood', - 'style', - 'albumlabel', - 'sorttitle', - 'episodeguide', - 'uniqueid', - 'dateadded', - 'channel', - 'channeltype', - 'hidden', - 'locked', - 'channelnumber', - 'starttime', - 'endtime', - 'specialsortseason', - 'specialsortepisode', - 'compilation', - 'releasetype', - 'albumreleasetype', - 'contributors', - 'displaycomposer', - 'displayconductor', - 'displayorchestra', - 'displaylyricist', - 'userrating' - ] - -} diff --git a/script.copacetic.helper/resources/lib/plugin/library.py b/script.copacetic.helper/resources/lib/plugin/library.py deleted file mode 100644 index 5ba45a098..000000000 --- a/script.copacetic.helper/resources/lib/plugin/library.py +++ /dev/null @@ -1,147 +0,0 @@ -# author: realcopacetic, sualfred - -import xbmc -from xbmcgui import ListItem - - -def add_items(li, json_query, type): - for item in json_query: - if type == 'movie': - set_movie(li, item) - elif type == 'tvshow': - set_tvshow(li, item) - elif type == 'episode': - set_episode(li, item) - elif type == 'musicvideo': - set_musicvideo(li, item) - - -def set_movie(li, item): - li_item = ListItem(item['title'], offscreen=True) - videoInfoTag = li_item.getVideoInfoTag() - videoInfoTag.setDbId(item['movieid']) - videoInfoTag.setDuration(item['runtime']) - videoInfoTag.setLastPlayed(item['lastplayed']) - videoInfoTag.setMediaType('movie') - videoInfoTag.setPlaycount(item['playcount']) - videoInfoTag.setResumePoint( - item['resume']['position'], item['resume']['total'] - ) - videoInfoTag.setTitle(item['title']) - videoInfoTag.setTrailer(item['trailer']) - videoInfoTag.setYear(item['year']) - videoInfoTag.setStudios(item['studio']) - videoInfoTag.setMpaa(item['mpaa']) - for key, value in iter(list(item['streamdetails'].items())): - for stream in value: - if 'video' in key: - videostream = xbmc.VideoStreamDetail(**stream) - videoInfoTag.addVideoStream(videostream) - elif 'audio' in key: - audiostreamlist = list(stream.values()) - audiostream = xbmc.AudioStreamDetail(*audiostreamlist) - videoInfoTag.addAudioStream(audiostream) - - li_item.setArt(item['art']) - li_item.setArt({'icon': 'DefaultMovies.png'}) - li.append((item['file'], li_item, False)) - - -def set_tvshow(li, item): - season = item['season'] - episode = int(item['episode']) - watchedepisodes = int(item['watchedepisodes']) - li_item = ListItem(item['title'], offscreen=True) - videoInfoTag = li_item.getVideoInfoTag() - videoInfoTag.setDbId(item['tvshowid']) - videoInfoTag.setLastPlayed(item['lastplayed']) - videoInfoTag.setMediaType('tvshow') - videoInfoTag.setTitle(item['title']) - videoInfoTag.setYear(item['year']) - videoInfoTag.setStudios(item['studio']) - videoInfoTag.setMpaa(item['mpaa']) - if episode > 0 and watchedepisodes > 0: - watchedepisodepercent = int(((watchedepisodes / episode) * 100)) - else: - watchedepisodepercent = 0 - - if episode > watchedepisodes: - unwatchedepisodes = int(episode - watchedepisodes) - else: - unwatchedepisodes = 0 - - li_item.setProperty('totalseasons', str(season)) - li_item.setProperty('totalepisodes', str(episode)) - li_item.setProperty('watchedepisodes', str(watchedepisodes)) - li_item.setProperty('unwatchedepisodes', str(unwatchedepisodes)) - li_item.setProperty('watchedepisodepercent', str(watchedepisodepercent)) - li_item.setArt(item['art']) - li_item.setArt({'icon': 'DefaultTVShows.png'}) - li.append((item['file'], li_item, True)) - - -def set_episode(li, item): - if item['episode'] < 10: - episode_number = f"0{item['episode']}" - else: - episode_number = item['episode'] - label = f"{item['season']}x{episode_number}" - li_item = ListItem(label, offscreen=True) - videoInfoTag = li_item.getVideoInfoTag() - videoInfoTag.setDbId(item['episodeid']) - videoInfoTag.setDuration(item['runtime']) - videoInfoTag.setEpisode(item['episode']) - videoInfoTag.setLastPlayed(item['lastplayed']) - videoInfoTag.setMediaType('episode') - videoInfoTag.setPlaycount(item['playcount']) - videoInfoTag.setPremiered(item['firstaired']) - videoInfoTag.setResumePoint( - item['resume']['position'], item['resume']['total'] - ) - videoInfoTag.setSeason(item['season']) - videoInfoTag.setTitle(item['title']) - videoInfoTag.setTvShowTitle(item['showtitle']) - videoInfoTag.setStudios(item['studio']) - videoInfoTag.setMpaa(item['mpaa']) - for key, value in iter(list(item['streamdetails'].items())): - for stream in value: - if 'video' in key: - videostream = xbmc.VideoStreamDetail(**stream) - videoInfoTag.addVideoStream(videostream) - elif 'audio' in key: - audiostreamlist = list(stream.values()) - audiostream = xbmc.AudioStreamDetail(*audiostreamlist) - videoInfoTag.addAudioStream(audiostream) - - li_item.setArt(item['art']) - li_item.setArt({'icon': 'DefaultTVShows.png'}) - li.append((item['file'], li_item, False)) - - -def set_musicvideo(li, item): - li_item = ListItem(item['title'], offscreen=True) - videoInfoTag = li_item.getVideoInfoTag() - videoInfoTag.setArtists(item['artist']) - videoInfoTag.setDbId(item['musicvideoid']) - videoInfoTag.setDuration(item['runtime']) - videoInfoTag.setLastPlayed(item['lastplayed']) - videoInfoTag.setMediaType('musicvideo') - videoInfoTag.setResumePoint( - item['resume']['position'], item['resume']['total'] - ) - videoInfoTag.setPlaycount(item['playcount']) - videoInfoTag.setTitle(item['title']) - videoInfoTag.setYear(item['year']) - for key, value in iter(list(item['streamdetails'].items())): - for stream in value: - if 'video' in key: - videostream = xbmc.VideoStreamDetail(**stream) - videoInfoTag.addVideoStream(videostream) - elif 'audio' in key: - audiostreamlist = list(stream.values()) - audiostream = xbmc.AudioStreamDetail(*audiostreamlist) - videoInfoTag.addAudioStream(audiostream) - - li_item.setArt(item['art']) - li_item.setArt({'icon': 'DefaultVideo.png'}) - li.append((item['file'], li_item, False)) diff --git a/script.copacetic.helper/resources/lib/plugin/listing.py b/script.copacetic.helper/resources/lib/plugin/listing.py deleted file mode 100644 index 33112fd2a..000000000 --- a/script.copacetic.helper/resources/lib/plugin/listing.py +++ /dev/null @@ -1,47 +0,0 @@ -# author: realcopacetic, sualfred - -import sys -from urllib.parse import urlencode - -import xbmcgui - -from resources.lib.utilities import ADDON, ADDON_ID, set_plugincontent - -LISTING = { - 'mixed': [ - {'name': ADDON.getLocalizedString(32601), 'info': 'in_progress'} - ], - 'tvshow': [ - {'name': ADDON.getLocalizedString(32600), 'info': 'next_up'} - ] -} - - -class PluginListing(object): - def __init__(self, params, li): - self.li = li - self.list_widgets() - - def list_widgets(self): - for category, widgets in list(LISTING.items()): - for item in widgets: - url = self._encode_url(info=item.get('info'), type=category) - self.plugin_category = item['name'] - self._add_item(item['name'], url) - - def _encode_url(self, **kwargs): - empty_keys = [key for key, value in list( - kwargs.items()) if not value or value is None] - for key in empty_keys: - del kwargs[key] - return '{0}?{1}'.format(sys.argv[0], urlencode(kwargs)) - - def _add_item(self, label, url): - icon = 'special://home/addons/' + ADDON_ID + '/resources/icon.png' - li_item = xbmcgui.ListItem(label=label, offscreen=True) - videoInfoTag = li_item.getVideoInfoTag() - videoInfoTag.setTitle(label) - videoInfoTag.setMediaType('video') - li_item.setArt({'icon': 'DefaultAddonVideo.png', 'thumb': icon}) - self.li.append((url, li_item, True)) - set_plugincontent(content='', category=self.plugin_category) diff --git a/script.copacetic.helper/resources/lib/plugin/main.py b/script.copacetic.helper/resources/lib/plugin/main.py deleted file mode 100644 index 9dd5dd913..000000000 --- a/script.copacetic.helper/resources/lib/plugin/main.py +++ /dev/null @@ -1,58 +0,0 @@ -# author: realcopacetic, sualfred - -import urllib.parse as urllib - -import xbmcplugin - -from resources.lib.plugin.content import * -from resources.lib.utilities import sys -from resources.lib.plugin.listing import PluginListing - - -class Main: - def __init__(self): - self._parse_argv() - self.info = self.params.get('info') - self.action = self.params.get('action') - if self.info: - self.getinfos() - elif self.action: - self.actions() - else: - self.listing() - - def _parse_argv(self): - path = sys.argv[2] - - try: - args = path[1:] - self.params = dict(urllib.parse_qsl(args)) - - ''' Workaround to get the correct values for titles with special characters - ''' - if ('title=\'\"' and '\"\'') in args: - start_pos = args.find('title=\'\"') - end_pos = args.find('\"\'') - clean_title = args[start_pos+8:end_pos] - self.params['title'] = clean_title - - except Exception: - self.params = {} - - def getinfos(self): - li = list() - plugin = PluginContent(self.params, li) - self._execute(plugin, self.info) - self._additems(li) - - def listing(self): - li = list() - PluginListing(self.params,li) - self._additems(li) - - def _execute(self, plugin, action): - getattr(plugin, action.lower())() - - def _additems(self, li): - xbmcplugin.addDirectoryItems(int(sys.argv[1]), li) - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) \ No newline at end of file diff --git a/script.copacetic.helper/resources/lib/script/actions.py b/script.copacetic.helper/resources/lib/script/actions.py deleted file mode 100644 index c76151d55..000000000 --- a/script.copacetic.helper/resources/lib/script/actions.py +++ /dev/null @@ -1,344 +0,0 @@ -# author: realcopacetic - -from resources.lib.service.art import ImageEditor -from resources.lib.utilities import (DIALOG, clear_playlists, condition, - infolabel, json_call, log_and_execute, - skin_string, window_property, xbmc) - - -def clean_filename(label=False, **kwargs): - json_response = json_call('Settings.GetSettingValue', - params={'setting': 'filelists.showextensions'}, - parent='clean_filename' - ) - - subtraction = 1 if json_response['result']['value'] is True else 0 - if not label: - label = infolabel('$INFO[ListItem.Label]') - count = label.count('.') - subtraction - label = label.replace('.', ' ', count).replace('_', ' ').strip() - - window_property('Return_Label', set=label) - - -def dialog_yesno(heading, message, **kwargs): - yes_actions = kwargs.get('yes_actions', '').split('|') - no_actions = kwargs.get('no_actions', 'Null').split('|') - - if DIALOG.yesno(heading, message): - for action in yes_actions: - log_and_execute(action) - else: - for action in no_actions: - log_and_execute(action) - - -def hex_contrast_check(**kwargs): - image = ImageEditor() - hex = kwargs.get('hex', '') - - if hex: - r = int(hex[2:-4], 16) - g = int(hex[4:-2], 16) - b = int(hex[6:], 16) - rgb = (r, g, b) - luminosity = image.return_luminosity(rgb) - best_contrast = 'dark' if luminosity > 0.179 else 'light' - - xbmc.executebuiltin( - f'Skin.SetString(Accent_Color_Contrast,{best_contrast})') - - -def play_album(**kwargs): - clear_playlists() - - dbid = int(kwargs.get('id', False)) - if dbid: - json_call('Player.Open', - item={'albumid': dbid}, - options={'shuffled': False}, - parent='play_album' - ) - - -def play_album_from_track(**kwargs): - clear_playlists() - - dbid = int(kwargs.get('id', False)) - track = int(kwargs.get('track', False)) - 1 - - if dbid: - json_response = json_call('AudioLibrary.GetSongDetails', - params={'properties': [ - 'albumid'], 'songid': dbid}, - parent='play_album_from_track' - ) - - if json_response['result'].get('songdetails', None): - albumid = json_response['result']['songdetails']['albumid'] - - json_call('Player.Open', - item={'albumid': albumid}, - options={'shuffled': False}, - parent='play_album_from_track' - ) - - if track > 0: - json_call('Player.GoTo', params={'playerid': 0, 'to': track}) - - -def play_items(id, **kwargs): - clear_playlists() - - method = kwargs.get('method', '') - shuffled = True if method == 'shuffle' else False - playlistid = 0 if kwargs.get('type', '') == 'music' else 1 - - if method == 'from_here': - method = f'Container({id}).ListItemNoWrap' - else: - method = f'Container({id}).ListItemAbsolute' - - for count in range(int(xbmc.getInfoLabel(f'Container({id}).NumItems'))): - - if xbmc.getCondVisibility(f'String.IsEqual({method}({count}).DBType,movie)'): - media_type = 'movie' - elif xbmc.getCondVisibility(f'String.IsEqual({method}({count}).DBType,episode)'): - media_type = 'episode' - elif xbmc.getCondVisibility(f'String.IsEqual({method}({count}).DBType,song)'): - media_type = 'song' - elif xbmc.getCondVisibility(f'String.IsEqual({method}({count}).DBType,musicvideo)'): - media_type = 'musicvideo' - - dbid = int(xbmc.getInfoLabel(f'{method}({count}).DBID')) - url = xbmc.getInfoLabel(f'{method}({count}).Filenameandpath') - - if media_type and dbid: - json_call('Playlist.Add', - item={f'{media_type}id': dbid}, - params={'playlistid': playlistid}, - parent='play_items' - ) - elif url: - json_call('Playlist.Add', - item={'file': url}, - params={'playlistid': playlistid}, - parent='play_items' - ) - - json_call('Playlist.GetItems', - params={'playlistid': playlistid}, - parent='play_items' - ) - - json_call('Player.Open', - item={'playlistid': playlistid, 'position': 0}, - options={'shuffled': shuffled}, - parent='play_items' - ) - - -def play_radio(**kwargs): - import random - clear_playlists() - - dbid = int(kwargs.get('id', xbmc.getInfoLabel('ListItem.DBID'))) - - json_response = json_call('AudioLibrary.GetSongDetails', - params={'properties': ['genre'], 'songid': dbid}, - parent='play_radio' - ) - - if json_response['result']['songdetails'].get('genre', None): - genre = json_response['result']['songdetails']['genre'] - genre = random.choice(genre) - - if genre: - json_call('Playlist.Add', - item={'songid': dbid}, - params={'playlistid': 0}, - parent='play_radio' - ) - - json_response = json_call('AudioLibrary.GetSongs', - params={'properties': ['genre']}, - sort={'method': 'random'}, - limit=24, - query_filter={'genre': genre}, - parent='play_radio' - ) - - for count in json_response['result']['songs']: - if count.get('songid', None): - songid = int(count['songid']) - - json_call('Playlist.Add', - item={'songid': songid}, - params={'playlistid': 0}, - parent='play_radio' - ) - - json_call('Playlist.GetItems', - params={'playlistid': 0}, - parent='play_radio' - ) - - json_call('Player.Open', - item={'playlistid': 0, 'position': 0}, - parent='play_radio' - ) - - -def rate_song(**kwargs): - dbid = int(kwargs.get('id', xbmc.getInfoLabel('ListItem.DBID'))) - rating_threshold = int(kwargs.get('rating', xbmc.getInfoLabel( - 'Skin.String(Music_Rating_Like_Threshold)'))) - - json_call('AudioLibrary.SetSongDetails', - params={'songid': dbid, 'userrating': rating_threshold}, - parent='rate_song' - ) - - player = xbmc.Player() - player_dbid = int(xbmc.getInfoLabel('MusicPlayer.DBID') - ) if player.isPlayingAudio() else None - - if dbid == player_dbid: - if rating_threshold != 0: - window_property('MusicPlayer_UserRating', set=rating_threshold) - else: - window_property('MusicPlayer_UserRating', clear=True) - ''' - player_path = player.getPlayingFile() - item = xbmcgui.ListItem(path=player_path) - musicInfoTag = item.getMusicInfoTag() - musicInfoTag.setUserRating(rating_threshold) - player.updateInfoTag(item) - ''' - - -def shuffle_artist(**kwargs): - clear_playlists() - - dbid = int(kwargs.get('id', False)) - json_call('Player.Open', - item={'artistid': dbid}, - options={'shuffled': True}, - parent='shuffle_artist') - - -def widget_move(posa, posb, **kwargs): - tempa_name = '' - tempa_target = '' - tempa_sortmethod = '' - tempa_sortorder = '' - tempa_path = '' - tempa_limit = '' - tempa_thumb = False, - tempb_name = '' - tempb_target = '' - tempb_sortmethod = '' - tempb_sortorder = '' - tempb_path = '' - tempb_limit = '' - tempb_thumb = False - - tempa_view = infolabel(f'Skin.String(Widget{posa}_View)') - tempa_display = infolabel(f'Skin.String(Widget{posa}_Display)') - tempb_view = infolabel(f'Skin.String(Widget{posb}_View)') - tempb_display = infolabel(f'Skin.String(Widget{posb}_Display)') - - if condition(f'Skin.HasSetting(Widget{posa}_Content_Disabled)'): - tempa_content = 'Disabled' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_InProgress)'): - tempa_content = 'InProgress' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_NextUp)'): - tempa_content = 'NextUp' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_LatestMovies)'): - tempa_content = 'LatestMovies' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_LatestTVShows)'): - tempa_content = 'LatestTVShows' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_RandomMovies)'): - tempa_content = 'RandomMovies' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_RandomTVShows)'): - tempa_content = 'RandomTVShows' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_LatestAlbums)'): - tempa_content = 'LatestAlbums' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_RecentAlbums)'): - tempa_content = 'RecentAlbums' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_RandomAlbums)'): - tempa_content = 'RandomAlbums' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_LikedSongs)'): - tempa_content = 'LikedSongs' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_Favourites)'): - tempa_content = 'Favourites' - elif condition(f'Skin.HasSetting(Widget{posa}_Content_Custom)'): - tempa_content = 'Custom' - tempa_name = infolabel(f'Skin.String(Widget{posa}_Custom_Name)') - tempa_target = infolabel(f'Skin.String(Widget{posa}_Custom_Target)') - tempa_sortmethod = infolabel(f'Skin.String(Widget{posa}_Custom_SortMethod)') - tempa_sortorder = infolabel(f'Skin.String(Widget{posa}_Custom_SortOrder)') - tempa_path = infolabel(f'Skin.String(Widget{posa}_Custom_Path)') - tempa_limit = infolabel(f'Skin.String(Widget{posa}_Custom_Limit)') - tempa_thumb = True if condition(f'Skin.HasSetting(Widget{posa}_Episode_Thumbs)') else False - - if condition(f'Skin.HasSetting(Widget{posb}_Content_Disabled)'): - tempb_content = 'Disabled' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_InProgress)'): - tempb_content = 'InProgress' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_NextUp)'): - tempb_content = 'NextUp' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_LatestMovies)'): - tempb_content = 'LatestMovies' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_LatestTVShows)'): - tempb_content = 'LatestTVShows' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_RandomMovies)'): - tempb_content = 'RandomMovies' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_RandomTVShows)'): - tempb_content = 'RandomTVShows' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_LatestAlbums)'): - tempb_content = 'LatestAlbums' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_RecentAlbums)'): - tempb_content = 'RecentAlbums' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_RandomAlbums)'): - tempb_content = 'RandomAlbums' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_LikedSongs)'): - tempb_content = 'LikedSongs' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_Favourites)'): - tempb_content = 'Favourites' - elif condition(f'Skin.HasSetting(Widget{posb}_Content_Custom)'): - tempb_content = 'Custom' - tempb_name = infolabel(f'Skin.String(Widget{posb}_Custom_Name)') - tempb_target = infolabel(f'Skin.String(Widget{posb}_Custom_Target)') - tempb_sortmethod = infolabel(f'Skin.String(Widget{posb}_Custom_SortMethod)') - tempb_sortorder = infolabel(f'Skin.String(Widget{posb}_Custom_SortOrder)') - tempb_path = infolabel(f'Skin.String(Widget{posb}_Custom_Path)') - tempb_limit = infolabel(f'Skin.String(Widget{posb}_Custom_Limit)') - tempb_thumb = True if condition(f'Skin.HasSetting(Widget{posb}_Episode_Thumbs)') else False - - xbmc.executebuiltin(f'Skin.ToggleSetting(Widget{posa}_Content_{tempa_content})') - xbmc.executebuiltin(f'Skin.SetBool(Widget{posa}_Content_{tempb_content})') - xbmc.executebuiltin(f'Skin.ToggleSetting(Widget{posb}_Content_{tempb_content})') - xbmc.executebuiltin(f'Skin.SetBool(Widget{posb}_Content_{tempa_content})') - skin_string(f'Widget{posb}_View', set=tempa_view) - skin_string(f'Widget{posa}_View', set=tempb_view) - skin_string(f'Widget{posb}_Display', set=tempa_display) - skin_string(f'Widget{posa}_Display', set=tempb_display) - skin_string(f'Widget{posb}_Custom_Name', set=tempa_name) - skin_string(f'Widget{posa}_Custom_Name', set=tempb_name) - skin_string(f'Widget{posb}_Custom_Target', set=tempa_target) - skin_string(f'Widget{posa}_Custom_Target', set=tempb_target) - skin_string(f'Widget{posb}_Custom_SortMethod', set=tempa_sortmethod) - skin_string(f'Widget{posa}_Custom_SortMethod', set=tempb_sortmethod) - skin_string(f'Widget{posb}_Custom_SortOrder', set=tempa_sortorder) - skin_string(f'Widget{posa}_Custom_SortOrder', set=tempb_sortorder) - skin_string(f'Widget{posb}_Custom_Path', set=tempa_path) - skin_string(f'Widget{posa}_Custom_Path', set=tempb_path) - skin_string(f'Widget{posb}_Custom_Limit', set=tempa_limit) - skin_string(f'Widget{posa}_Custom_Limit', set=tempb_limit) - if tempa_thumb and not tempb_thumb: - xbmc.executebuiltin(f'Skin.ToggleSetting(Widget{posa}_Episode_Thumbs)') - xbmc.executebuiltin(f'Skin.SetBool(Widget{posb}_Episode_Thumbs)') - elif tempb_thumb and not tempa_thumb: - xbmc.executebuiltin(f'Skin.ToggleSetting(Widget{posb}_Episode_Thumbs)') - xbmc.executebuiltin(f'Skin.SetBool(Widget{posa}_Episode_Thumbs)') diff --git a/script.copacetic.helper/resources/lib/service/art.py b/script.copacetic.helper/resources/lib/service/art.py deleted file mode 100644 index e79d14c6e..000000000 --- a/script.copacetic.helper/resources/lib/service/art.py +++ /dev/null @@ -1,300 +0,0 @@ -# author: realcopacetic - -import hashlib -import random -import urllib.parse as urllib -import xml.etree.ElementTree as ET - -from PIL import Image - -from resources.lib.utilities import (CROPPED_FOLDERPATH, LOOKUP_XML, - TEMP_FOLDERPATH, infolabel, json_call, - log, os, validate_path, - window_property, xbmc, xbmcvfs) - - -class ImageEditor(): - def __init__(self): - self.clearlogo_bbox = (600, 240) - self.cropped_folder = CROPPED_FOLDERPATH - self.temp_folder = TEMP_FOLDERPATH - self.lookup = LOOKUP_XML - - def clearlogo_cropper(self, url=False, type='clearlogo', source='ListItem', return_color=False, reporting=window_property, reporting_key=None): - # establish clearlogo urls - if url: - clearlogos = {type: url} - else: - clearlogos = { - 'clearlogo': False, - 'clearlogo-alt': False, - 'clearlogo-billboard': False - } - if source == 'ListItem' or source == 'VideoPlayer': - path = source - else: - path = f'Container({source}).ListItem' - for key in clearlogos: - url = xbmc.getInfoLabel(f'{path}.Art({key})') - if url: - clearlogos[key] = url - # lookup urls in table or run _crop_image() and write values to table - lookup_tree = ET.parse(self.lookup) - root = lookup_tree.getroot() - for key, value in list(clearlogos.items()): - self.id = xbmc.getInfoLabel(f'{path}.dbid') - self.destination, self.height, self.color, self.luminosity = False, False, False, False - name = reporting_key or key - if value: - for node in root.find('clearlogos'): - if value in node.attrib['name'] and validate_path(node.find('path').text): - self.destination = node.find('path').text - self.height = node.find('height').text - self.color = node.find('color').text - self.luminosity = node.find('luminosity').text - break - else: - self._crop_image(value) - clearlogo = ET.SubElement( - root.find('clearlogos'), 'clearlogo') - clearlogo.attrib['name'] = value - path = ET.SubElement(clearlogo, 'path') - path.text = self.destination - height = ET.SubElement(clearlogo, 'height') - height.text = str(self.height) - color = ET.SubElement(clearlogo, 'color') - color.text = self.color - luminosity = ET.SubElement(clearlogo, 'luminosity') - luminosity.text = str(self.luminosity) - lookup_tree.write(self.lookup, encoding="utf-8") - reporting(key=f'{name}_cropped', set=self.destination) - reporting(key=f'{name}_cropped-id', set=self.id) - reporting(key=f'{name}_cropped-height', set=self.height) - if return_color: - reporting(key=f'{name}_cropped-color', set=self.color) - reporting(key=f'{name}_cropped-luminosity', - set=self.luminosity) - - def return_luminosity(self, rgb): - # Credit to Mark Ransom for luminosity calculation - # https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color - new_rgb = () - for channel in rgb: - c = channel / 255.0 - if c <= 0.04045: - output = c / 12.92 - else: - output = pow(((c + 0.055) / 1.055), 2.4) - new_rgb += (output,) - r, g, b = new_rgb - luminosity = 0.2126 * r + 0.7152 * g + 0.0722 * b - return luminosity - - def _crop_image(self, url): - filename = f'{hashlib.md5(url.encode()).hexdigest()}.png' - self.destination = os.path.join(self.cropped_folder, filename) - # If crop exists, open to get height and color - if validate_path(self.destination): - image = self._open_image(self.destination) - self._image_functions(image) - # else get image url, open and crop, then get height and color - else: - url = self._return_image_path(url, '.png') - try: - image = self._open_image(url) - except Exception as error: - log( - f'ImageEditor: Error - could not open cached image --> {error}', force=True) - else: - if image.mode == 'LA': # Convert if mode == 'LA' - converted_image = Image.new("RGBA", image.size) - converted_image.paste(image) - image = converted_image - image = image.crop(image.convert('RGBa').getbbox()) - with xbmcvfs.File(self.destination, 'wb') as f: - image.save(f, 'PNG') - self._image_functions(image) - log( - f'ImageEditor: Image cropped and saved: {url} --> {self.destination}') - if self.temp_folder in url: # If temp file created, delete it now - xbmcvfs.delete(url) - log(f'ImageEditor: Temporary file deleted --> {url}') - - def _return_image_path(self, source, suffix): - # Use source URL to generate cached url. If cached url doesn't exist, return source url - cleaned_source = self.url_decode_path(source) - cached_thumb = xbmc.getCacheThumbName( - cleaned_source).replace('.tbn', '') - cached_url = os.path.join( - 'special://profile/Thumbnails/', f'{cached_thumb[0]}/', cached_thumb + suffix) - if validate_path(cached_url): - return cached_url - else: - # Create temp file to avoid access issues to direct source - filename = f'{hashlib.md5(cleaned_source.encode()).hexdigest()}.png' - destination = os.path.join(self.temp_folder, filename) - if not validate_path(destination): - xbmcvfs.copy(cleaned_source, destination) - log(f'ImageEditor: Temporary file created --> {destination}') - return destination - - def url_decode_path(self, path): - path = path[:-1] if path.endswith('/') else path - path = urllib.unquote(path.replace('image://', '')) - return path - - def _open_image(self, url): - image = Image.open(xbmcvfs.translatePath(url)) - return image - - def _image_functions(self, image): - self.height = self._return_scaled_height(image) - self.color, self.luminosity = self._return_dominant_color(image) - image.close() - - def _return_scaled_height(self, image): - image.thumbnail(self.clearlogo_bbox) - size = image.size - height = size[1] - return height - - def _return_dominant_color(self, image): - width, height = 75, 30 - image.thumbnail((width, height)) - # Remove transparent pixels - pixeldata = image.getcolors(width * height) - sorted_pixeldata = sorted(pixeldata, key=lambda t: t[0], reverse=True) - opaque_pixeldata = [ - pixeldata for pixeldata in sorted_pixeldata if pixeldata[-1][-1] > 64] - opaque_pixels = [] - for position, pixeldata in enumerate(opaque_pixeldata): - for count in range(pixeldata[0]): - opaque_pixels.append(pixeldata[1]) - # Reduce colors to palette - paletted = Image.new('RGBA', (len(opaque_pixels), 1)) - paletted.putdata(opaque_pixels) - paletted = paletted.convert( - 'P', palette=Image.ADAPTIVE, colors=16) - # Find color that occurs most often - palette = paletted.getpalette() - color_counts = sorted(paletted.getcolors(), reverse=True) - try: - palette_index = color_counts[0][1] - except IndexError as error: - log(f'ImageEditor: Error - could not calculate dominant colour for {infolabel("ListItem.Label")} --> {error}', force=True) - return (False, False) - else: - # Convert to rgb and calculate luminosity - dominant = palette[palette_index*3:palette_index*3+3] - luminosity = self.return_luminosity(dominant) - luminosity = int(luminosity * 1000) - dominant = self._rgb_to_hex(dominant) - return (dominant, luminosity) - - def _rgb_to_hex(self, rgb): - red, green, blue = rgb - hex = 'ff%02x%02x%02x' % (red, green, blue) - return hex - - def _return_average_color(self, image): - h = image.histogram() - # split into red, green, blue - r = h[0:256] - g = h[256:256*2] - b = h[256*2: 256*3] - # perform the weighted average of each channel: - # the *index* is the channel value, and the *value* is its weight - return ( - sum(i*w for i, w in enumerate(r)) / sum(r), - sum(i*w for i, w in enumerate(g)) / sum(g), - sum(i*w for i, w in enumerate(b)) / sum(b) - ) - - -class SlideshowMonitor: - def __init__(self): - self.refresh_count = self.refresh_interval = self._get_refresh_interval() - self.fetch_count = self.fetch_interval = self.refresh_interval * 30 - - def background_slideshow(self): - # Check if refresh interval has been adjusted in skin settings - if self.refresh_interval != self._get_refresh_interval(): - self.refresh_count = self.refresh_interval = self._get_refresh_interval() - self.fetch_count = self.fetch_interval = self.refresh_interval * 30 - # Fech art every 30 x refresh interval - if self.fetch_count >= self.fetch_interval: - log('Monitor fetching background art') - self.art = self._get_art() - self.fetch_interval = len(self.art) if (len(self.art) < 30) else self.fetch_interval - self.fetch_count = 0 - else: - self.fetch_count += 1 - # Set art every refresh interval - if self.refresh_count >= self.refresh_interval: - if self.art.get('all'): - self._set_art('Background_Global', self.art['all']) - if self.art.get('movies'): - self._set_art('Background_Movies', self.art['movies']) - if self.art.get('tvshows'): - self._set_art('Background_TVShows', self.art['tvshows']) - if self.art.get('videos'): - self._set_art('Background_Videos', self.art['videos']) - if self.art.get('artists'): - self._set_art('Background_Artists', self.art['artists']) - self.refresh_count = 0 - else: - self.refresh_count += 1 - - def _get_refresh_interval(self): - try: - self.refresh_interval = int( - infolabel('Skin.String(Background_Interval)') - ) - except ValueError: - self.refresh_interval = 10 - return self.refresh_interval - - def _get_art(self): - self.art = {} - self.art['movies'] = [] - self.art['tvshows'] = [] - self.art['artists'] = [] - self.art['musicvideos'] = [] - self.art['videos'] = [] - self.art['all'] = [] - for item in ['movies', 'tvshows', 'artists', 'musicvideos']: - dbtype = 'Video' if item != 'artists' else 'Audio' - query = json_call(f'{dbtype}Library.Get{item}', properties=['art'], sort={ - 'method': 'random'}, limit=40, parent='get_art') - try: - for result in query['result'][item]: - if result['art'].get('fanart'): - data = {'title': result.get('label', '')} - data.update(result['art']) - self.art[item].append(data) - except KeyError: - pass - self.art['videos'] = self.art['movies'] + self.art['tvshows'] - for list in self.art: - if self.art[list]: - self.art['all'] = self.art['all'] + self.art[list] - return self.art - - def _set_art(self, key, items): - art = random.choice(items) - # fanart = self._url_decode_path(art.get('fanart')) - fanarts = {key: value for (key, value) in art.items() if 'fanart' in key} - fanart = random.choice(list(fanarts.values())) - fanart = self._url_decode_path(fanart) - window_property(f'{key}_Fanart', set=fanart) - # clearlogo if present otherwise clear - clearlogo = art.get('clearlogo', False) - if clearlogo: - clearlogo = self._url_decode_path(clearlogo) - window_property(f'{key}_Clearlogo', set=clearlogo) - - def _url_decode_path(self, path): - path = path[:-1] if path.endswith('/') else path - path = path.replace('image://', '') - path = urllib.unquote(path.replace('image://', '')) - return path diff --git a/script.copacetic.helper/resources/lib/service/monitor.py b/script.copacetic.helper/resources/lib/service/monitor.py deleted file mode 100644 index 685920755..000000000 --- a/script.copacetic.helper/resources/lib/service/monitor.py +++ /dev/null @@ -1,256 +0,0 @@ -# author: realcopacetic - -import xml.etree.ElementTree as ET - -import xbmc - -from resources.lib.service.art import ImageEditor, SlideshowMonitor -from resources.lib.service.player import PlayerMonitor -from resources.lib.service.settings import SettingsMonitor -from resources.lib.utilities import (CROPPED_FOLDERPATH, LOOKUP_XML, - TEMP_FOLDERPATH, condition, create_dir, - get_cache_size, infolabel, log, - log_and_execute, split, - split_random_return, validate_path, - window_property) - -XMLSTR = ''' - - - -''' - - -class Monitor(xbmc.Monitor): - def __init__(self): - # Poller - self.start = True - self.idle = False - self.check_settings, self.check_cache = True, True - self.position, self.dbid, self.dbtype = False, False, False - # Setup - self.cropped_folder = CROPPED_FOLDERPATH - self.temp_folder = TEMP_FOLDERPATH - self.lookup = LOOKUP_XML - # Monitors - self.player_monitor = None - self.settings_monitor = SettingsMonitor() - self.art_monitor = SlideshowMonitor() - self._clearlogo_cropper = ImageEditor().clearlogo_cropper - # Run - self._create_dirs() - self._on_start() - - def _create_dirs(self): - if not validate_path(self.cropped_folder): - create_dir(self.cropped_folder) - if not validate_path(self.temp_folder): - create_dir(self.temp_folder) - if not validate_path(self.lookup): - root = ET.fromstring(XMLSTR) - ET.ElementTree(root).write( - self.lookup, xml_declaration=True, encoding="utf-8") - - def _on_start(self): - if self.start: - log('Monitor started', force=True) - self.start = False - self.player_monitor = PlayerMonitor() - else: - log('Monitor resumed', force=True) if self._conditions_met() else None - while not self.abortRequested() and self._conditions_met(): - self.poller() - self._on_stop() - - def _conditions_met(self): - return ( - self._get_skindir() and not self.idle and - ( - condition('!Skin.HasSetting(Background_Disabled)') or - condition('Skin.HasSetting(Crop_Clearlogos)') - ) - ) - - def _get_skindir(self): - skindir = xbmc.getSkinDir() - if 'skin.copacetic' in skindir: - return True - - def _get_info(self): - split_random_return( - infolabel('ListItem.Director'), name='RandomDirector') - split_random_return( - infolabel('ListItem.Genre'), name='RandomGenre') - split(infolabel('ListItem.Writer'), name='WriterSplit') - split(infolabel('ListItem.Studio'), name='StudioSplit') - - def poller(self): - # video playing fullscreen - if condition( - 'VideoPlayer.IsFullscreen' - ): - self.waitForAbort(1) - - # info screen visible and main menu selected - elif condition( - '[Window.Is(movieinformation) | ' - 'Window.Is(musicinformation) | ' - 'Window.Is(songinformation)] + ![' - 'Control.HasFocus(3201) | ' - 'Control.HasFocus(3202) | ' - 'Control.HasFocus(3203) | ' - 'Control.HasFocus(3204) | ' - 'Control.HasFocus(3205) | ' - 'Control.HasFocus(3206) | ' - 'Control.HasFocus(3207) | ' - 'Control.HasFocus(3208) | ' - 'Control.HasFocus(3209)]' - ): - self._on_scroll(crop=False, return_color=False, get_info=True) - self.waitForAbort(0.2) - - # secondary list has focus and clearlogo view visible - elif condition( - 'Skin.HasSetting(Crop_Clearlogos) + ' - 'Control.HasFocus(3100) + [' - 'Control.IsVisible(501) | Control.IsVisible(502) | Control.IsVisible(504)]' - ): - self._on_scroll(key='3100', return_color=False) - self.waitForAbort(0.2) - - # clearlogo view visible - elif condition( - 'Skin.HasSetting(Crop_Clearlogos) + [' - 'Control.IsVisible(501) | ' - 'Control.IsVisible(502) | ' - 'Control.IsVisible(504)]' - ): - self._on_scroll() - self.waitForAbort(0.2) - - # home widgets has clearlogo visible - elif condition( - 'Window.Is(home) + ' - 'Skin.HasSetting(Crop_Clearlogos) + [' - 'Control.HasFocus(3201) | ' - 'Control.HasFocus(3202) | ' - 'Control.HasFocus(3203) | ' - 'Control.HasFocus(3204) | ' - 'Control.HasFocus(3205) | ' - 'Control.HasFocus(3206) | ' - 'Control.HasFocus(3207) | ' - 'Control.HasFocus(3208) | ' - 'Control.HasFocus(3209)]' - ): - widget = infolabel('System.CurrentControlID') - self._on_scroll(key=widget) - self.waitForAbort(0.2) - - # slideshow window is visible run SlideshowMonitor() - elif condition( - '!Skin.HasSetting(Background_Disabled) + [' - 'Window.IsVisible(home) | ' - 'Window.IsVisible(settings) | ' - 'Window.IsVisible(skinsettings) | ' - 'Window.IsVisible(appearancesettings) | ' - 'Window.IsVisible(mediasettings) | ' - 'Window.IsVisible(playersettings) | ' - 'Window.IsVisible(servicesettings) | ' - 'Window.IsVisible(systemsettings) | ' - 'Window.IsVisible(pvrsettings) | ' - 'Window.IsVisible(gamesettings) | ' - 'Window.IsVisible(profiles) | ' - 'Window.IsVisible(systeminfo) | ' - 'Window.IsVisible(filemanager) | ' - 'Window.IsVisible(addonsettings) + !String.IsEmpty(ListItem.Art(fanart)) | ' - 'Window.IsVisible(addonbrowser) + !Container.Content(addons) | ' - 'Window.IsVisible(mediasource) | ' - 'Window.IsVisible(smartplaylisteditor) | ' - 'Window.IsVisible(musicplaylisteditor) | ' - 'Window.IsVisible(mediasource) | ' - 'Window.IsVisible(tvguide) | ' - 'Container.Content(genres) | ' - 'Container.Content(years) | ' - 'Container.Content(playlists) | ' - 'Container.Content(sources) | ' - 'Container.Content(studios) | ' - 'Container.Content(directors) | ' - 'Container.Content(tags) | ' - 'Container.Content(countries) | ' - 'Container.Content(roles) | ' - 'Container.Content() + [Window.Is(videos) | Window.Is(music)]]' - ): - self.art_monitor.background_slideshow() - self._on_skinsettings() - self._on_recommendedsettings() - self.waitForAbort(1) - - # else wait for next poll - else: - self.check_cache = True - self.check_settings = True - self.waitForAbort(1) - - def _on_scroll(self, key='ListItem', crop=True, return_color=True, get_info=False): - path, current_item, current_dbid, current_dbtype = self._current_item( - key) - if ( - current_item != self.position or - current_dbid != self.dbid or - current_dbtype != self.dbtype - ) and not self._container_scrolling(key): - if crop: - self._clearlogo_cropper( - source=key, return_color=return_color, reporting=window_property) - if get_info: - self._get_info() - self.position = current_item - self.dbid = current_dbid - self.dbtype = current_dbtype - - def _on_skinsettings(self): - if condition('Window.Is(skinsettings)') and self.check_cache: - get_cache_size() - self.check_cache = False - elif condition('!Window.Is(skinsettings)'): - self.check_cach = True - - def _on_recommendedsettings(self): - if condition('Window.Is(skinsettings)') and self.check_settings: - self.settings_monitor.get_default() - self.check_settings = False - elif not condition('Window.Is(skinsettings)'): - self.check_settings = True - if condition('Skin.HasSetting(run_set_default)'): - self.settings_monitor.set_default() - self.check_settings = True - log_and_execute('Skin.ToggleSetting(run_set_default)') - - def _on_stop(self): - log(f'Monitor idle', force=True) - while not self.abortRequested() and not self._conditions_met(): - self.waitForAbort(2) - if not self.abortRequested(): - self._on_start() - else: - del self.player_monitor - del self.settings_monitor - del self.art_monitor - log(f'Monitor stopped', force=True) - - def _current_item(self, key='ListItem'): - container = 'Container' if key == 'ListItem' else f'Container({key})' - item = infolabel(f'{container}.CurrentItem') - dbid = infolabel(f'{container}.ListItem.DBID') - dbtype = infolabel(f'{container}.ListItem.DBType') - return (container, item, dbid, dbtype) - - def _container_scrolling(self, key='ListItem'): - container = 'Container' if key == 'ListItem' else f'Container({key})' - return condition(f'{container}.Scrolling') - - def onScreensaverActivated(self): - self.idle = True - - def onScreensaverDeactivated(self): - self.idle = False diff --git a/script.copacetic.helper/resources/lib/service/player.py b/script.copacetic.helper/resources/lib/service/player.py deleted file mode 100644 index 0775b2a8a..000000000 --- a/script.copacetic.helper/resources/lib/service/player.py +++ /dev/null @@ -1,41 +0,0 @@ -# author: realcopacetic - -from xbmc import Player - -from resources.lib.script.actions import clean_filename -from resources.lib.service.art import ImageEditor -from resources.lib.utilities import condition, window_property - - -class PlayerMonitor(Player): - def __init__(self): - Player.__init__(self) - self.clearlogo_cropper = ImageEditor().clearlogo_cropper - - def onAVStarted(self): - if self.isPlayingVideo() and condition('String.IsEmpty(Window(home).Property(Trailer_Autoplay))'): - # Crop clearlogo for use on fullscreen info or pause - self.clearlogo_cropper(source='VideoPlayer', - reporting=window_property) - - # Clean filename - item = self.getPlayingItem() - label = item.getLabel() - if label: - clean_filename(label=label) - else: - window_property('Return_Label', clear=True) - - # Get user rating on music playback - if self.isPlayingAudio(): - tag = self.getMusicInfoTag() - user_rating = tag.getUserRating() - album_artist = tag.getAlbumArtist() - window_property('MusicPlayer_UserRating', set=user_rating) - window_property('MusicPlayer_AlbumArtist', set=album_artist) - - def onPlayBackStopped(self): - # Clean properties - window_property('MusicPlayer_UserRating', clear=True) - window_property('MusicPlayer_AlbumArtist', clear=True) - window_property('Return_Label', clear=True) diff --git a/script.copacetic.helper/resources/lib/service/settings.py b/script.copacetic.helper/resources/lib/service/settings.py deleted file mode 100644 index 450772501..000000000 --- a/script.copacetic.helper/resources/lib/service/settings.py +++ /dev/null @@ -1,84 +0,0 @@ -# author: realcopacetic - -from resources.lib.utilities import (ADDON, ADDON_ID, DIALOG, condition, - json_call, log, window_property) - - -class SettingsMonitor: - def __init__(self): - self.settings = { - 'filelists.showparentdiritems': False, - 'videolibrary.showallitems': False, - 'videolibrary.groupmoviesets': True, - 'videolibrary.flattentvshows': 1, - 'videolibrary.showemptytvshows': False, - 'videolibrary.tvshowsselectfirstunwatcheditem': 2, - 'videolibrary.tvshowsincludeallseasonsandspecials': 3, - 'videolibrary.artworklevel': 2, - 'videolibrary.movieartwhitelist': ['keyart', 'square', 'clearlogo', 'clearlogo-alt', 'clearlogo-billboard'], - 'videolibrary.tvshowartwhitelist': ['keyart', 'square', 'clearlogo', 'clearlogo-alt', 'clearlogo-billboard'], - 'musiclibrary.showallitems': False, - 'musiclibrary.showcompilationartists': False, - 'pictures.generatethumbs': True, - 'musicplayer.visualisation': 'visualization.waveform' - } - self.settings_to_change = {} - - def get_default(self, **kwargs): - self.settings_to_change.clear() - window_property('Settings_To_Change') - cats = { - 'general': False, - 'videolibrary': False, - 'musiclibrary': False, - 'pictures': False, - 'musicplayer': False - } - for item in list(self.settings.items()): - window_property(key=item[0]) - category = item[0].split('.')[0] - json_response = json_call('Settings.GetSettingValue', - params={'setting': item[0]}, - parent='get_settings' - ) - try: - value = json_response['result']['value'] - - except KeyError: - value = 'None' - if value != item[1]: - if ( - (isinstance(value, int) and value == 0) or - (isinstance(value, bool) and value == False) - ): - value = '0' - elif ( - (isinstance(value, list) and value == []) or - (isinstance(value, str) and value == '') - ): - value = 'None' - cats.update({category: True}) - self.settings_to_change.update({item[0]: item[1]}) - if isinstance(value, list): - window_property(key=item[0], set=', '.join(value)) - else: - window_property(key=item[0], set=value) - cat_count = sum(value for value in list(cats.values())) - item_count = cat_count + len(self.settings_to_change) - window_property('Settings_To_Change', set=item_count) - - def set_default(self, **kwargs): - count = 0 - for item in list(self.settings_to_change.items()): - if condition(f'Skin.HasSetting({item[0]})'): - json_call('Settings.SetSettingValue', - params={'setting': item[0], 'value': item[1]}, - parent='set_settings' - ) - count += 1 - if count == 1: - string = ADDON.getLocalizedString(32203) - else: - string = ADDON.getLocalizedString(32204) - string = f'{count} ' + string + '.' - DIALOG.notification(ADDON_ID, string) diff --git a/script.copacetic.helper/resources/lib/utilities.py b/script.copacetic.helper/resources/lib/utilities.py deleted file mode 100644 index 84d5df255..000000000 --- a/script.copacetic.helper/resources/lib/utilities.py +++ /dev/null @@ -1,243 +0,0 @@ -# author: realcopacetic - - -import json -import os -import sys -import urllib.parse as urllib - -import xbmc -import xbmcvfs -from xbmcaddon import Addon -from xbmcgui import Dialog, Window -from xbmcplugin import (SORT_METHOD_LASTPLAYED, addSortMethod, setContent, - setPluginCategory) - -ADDON = Addon() -ADDON_ID = ADDON.getAddonInfo('id') -ADDONDATA = 'special://profile/addon_data/script.copacetic.helper/' -ADDONDATA = xbmcvfs.validatePath( - xbmcvfs.translatePath(ADDONDATA)) -CROPPED_FOLDERPATH = os.path.join(ADDONDATA, 'crop/') -TEMP_FOLDERPATH = os.path.join(ADDONDATA, 'temp/') -LOOKUP_XML = os.path.join(ADDONDATA, '_lookup.xml') - -DEBUG = xbmc.LOGDEBUG -INFO = xbmc.LOGINFO -WARNING = xbmc.LOGWARNING -ERROR = xbmc.LOGERROR - -DIALOG = Dialog() -VIDEOPLAYLIST = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) -MUSICPLAYLIST = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - - -def clear_playlists(): - log('Clear playlists') - VIDEOPLAYLIST.clear() - MUSICPLAYLIST.clear() - MUSICPLAYLIST.unshuffle() - - -def condition(condition): - return xbmc.getCondVisibility(condition) - - -def get_folder_size(source=CROPPED_FOLDERPATH): - bytes = 0 - if xbmcvfs.exists(source): - dirs, files = xbmcvfs.listdir(source) - for filename in files: - path = os.path.join(source, filename) - item = xbmcvfs.File(path) - size = item.size() - bytes += size - item.close() - return bytes - - -def get_cache_size(precision=1): - temp_size, crop_size = 0, 0 - if xbmcvfs.exists(TEMP_FOLDERPATH): - temp_size = get_folder_size(source=TEMP_FOLDERPATH) - if xbmcvfs.exists(CROPPED_FOLDERPATH): - crop_size = get_folder_size(source=CROPPED_FOLDERPATH) - size = temp_size + crop_size - ''' Credit Doug Latornell for bitshift method - https://code.activestate.com/recipes/577081-humanized-representation-of-a-number-of-bytes/ - ''' - abbrevs = ( - (1 << 30, 'GB'), - (1 << 20, 'MB'), - (1 << 10, 'KB'), - (1, 'bytes') - ) - for factor, suffix in abbrevs: - if size >= factor: - break - readable = '%.*f %s' % (precision, size / factor, - suffix) if size > 0 else '0.0 bytes' - window_property('Addon_Data_Folder_Size', set=readable) - return readable - - -def validate_path(path): - return xbmcvfs.exists(path) - - -def create_dir(path): - try: # Try makedir to avoid race conditions - xbmcvfs.mkdirs(path) - except FileExistsError: - return - - -def clear_cache(**kwargs): - import xml.etree.ElementTree as ET - - # remove temp and crop folders - readable_size = get_cache_size() - if xbmcvfs.exists(TEMP_FOLDERPATH): - xbmcvfs.rmdir(TEMP_FOLDERPATH, force=True) - create_dir(TEMP_FOLDERPATH) - if xbmcvfs.exists(CROPPED_FOLDERPATH): - xbmcvfs.rmdir(CROPPED_FOLDERPATH, force=True) - create_dir(CROPPED_FOLDERPATH) - log(f'Clearlogo cache cleared by user. {readable_size} saved.') - string = ADDON.getLocalizedString( - 32201) + f', {readable_size} ' + ADDON.getLocalizedString(32202) + '.' - DIALOG.notification(ADDON_ID, string) - # Update cache label - get_cache_size() - # Remove old clearlogos from lookup table - lookup_tree = ET.parse(LOOKUP_XML) - root = lookup_tree.getroot() - del root[0] - ET.SubElement(root, 'clearlogos') - lookup_tree.write(LOOKUP_XML, encoding="utf-8") - - -def get_joined_items(item): - if len(item) > 0 and item is not None: - item = ' / '.join(item) - else: - item = '' - return item - - -def infolabel(infolabel): - return xbmc.getInfoLabel(infolabel) - - -def json_call(method, properties=None, sort=None, query_filter=None, limit=None, params=None, item=None, options=None, limits=None, parent=None, debug=False): - json_string = {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': {}} - - if properties is not None: - json_string['params']['properties'] = properties - if limit is not None: - json_string['params']['limits'] = {'start': 0, 'end': int(limit)} - if sort is not None: - json_string['params']['sort'] = sort - if query_filter is not None: - json_string['params']['filter'] = query_filter - if options is not None: - json_string['params']['options'] = options - if limits is not None: - json_string['params']['limits'] = limits - if item is not None: - json_string['params']['item'] = item - if params is not None: - json_string['params'].update(params) - - jsonrpc_call = json.dumps(json_string) - result = xbmc.executeJSONRPC(jsonrpc_call) - result = json.loads(result) - - if (ADDON.getSettingBool('json_logging') or debug): - log(f'JSON call for function {parent} ' + - pretty_print(json_string), force=debug) - log(f'JSON result for function {parent} ' + - pretty_print(result), force=debug) - return result - - -def pretty_print(string): - return json.dumps(string, sort_keys=True, indent=4, separators=(',', ': ')) - - -def log(message, loglevel=DEBUG, force=False): - if (ADDON.getSettingBool('debug_logging') or force) and loglevel not in [WARNING, ERROR]: - loglevel = INFO - xbmc.log(f'{ADDON_ID} --> {message}', loglevel) - - -def log_and_execute(action): - log(f'Execute: {action}', DEBUG) - xbmc.executebuiltin(action) - - -def return_label(property=True, **kwargs): - - label = kwargs.get('label', xbmc.getInfoLabel('ListItem.Label')) - find = kwargs.get('find', '.') - replace = kwargs.get('replace', ' ') - - count = label.count(find) - label = label.replace(urllib.unquote(find), - urllib.unquote(replace), - count) - if property: - window_property('Return_Label', set=label) - else: - return label - - -def set_plugincontent(content=None, category=None): - if category: - setPluginCategory(int(sys.argv[1]), category) - if content: - setContent(int(sys.argv[1]), content) - if category == ADDON.getLocalizedString(32601): - addSortMethod(int(sys.argv[1]), SORT_METHOD_LASTPLAYED) - - -def skin_string(key, set=False, clear=False, debug=False): - if set: - xbmc.executebuiltin(f'Skin.SetString({key}, {set})') - log(f'Skin string: Set, {key}, {set}', force=debug) - else: - xbmc.executebuiltin(f"Skin.SetString({key},)") - log(f'Skin string: Clear, {key}', force=debug) - - -def split(string, **kwargs): - separator = kwargs.get('separator', ' / ') - name = kwargs.get('name', 'Split') - - for count, value in enumerate(string.split(separator)): - window_property(f'{name}.{count}', set=value) - - -def split_random_return(string, **kwargs): - import random - - separator = kwargs.get('separator', ' / ') - name = kwargs.get('name', 'SplitRandomReturn') - string = random.choice(string.split(separator)) - random = random.choice(string.split(' & ')) - random = return_label(label=random, find='-', replace=' ', - property=False) if random != 'Sci-Fi' else random - random = random.strip() - - window_property(name, set=random) - return random - - -def window_property(key, set=False, clear=False, window_id=10000, debug=False): - window = Window(window_id) - if set: - window.setProperty(key, f'{set}') - log(f'Window property: Set, {window_id}, {key}, {set}', force=debug) - else: - window.clearProperty(key) - log(f'Window property: Clear, {window_id}, {key}', force=debug) diff --git a/script.copacetic.helper/resources/settings.xml b/script.copacetic.helper/resources/settings.xml deleted file mode 100644 index 3d576ea43..000000000 --- a/script.copacetic.helper/resources/settings.xml +++ /dev/null @@ -1,22 +0,0 @@ - - -
- - - - 0 - false - - - - 0 - false - - - true - - - - -
-
\ No newline at end of file diff --git a/script.copacetic.helper/script.py b/script.copacetic.helper/script.py deleted file mode 100644 index b37040722..000000000 --- a/script.copacetic.helper/script.py +++ /dev/null @@ -1,18 +0,0 @@ -# author: realcopacetic - -from resources.lib.script.actions import * -from resources.lib.utilities import clear_cache, sys - - -class Main: - def __init__(self, *args): - try: - self.params = dict(arg.split('=', 1) for arg in args) - except: - self.params = {} - function = eval(self.params['action']) - function(**self.params) - - -if __name__ == '__main__': - Main(*sys.argv[1:]) diff --git a/script.copacetic.helper/service.py b/script.copacetic.helper/service.py deleted file mode 100644 index b3eb56ff4..000000000 --- a/script.copacetic.helper/service.py +++ /dev/null @@ -1,6 +0,0 @@ -# author: realcopacetic - -from resources.lib.service.monitor import Monitor - -if __name__ == "__main__": - Monitor() diff --git a/script.module.dropbox_auth/LICENSE.txt b/script.module.dropbox_auth/LICENSE.txt deleted file mode 100644 index aeba2b6e9..000000000 --- a/script.module.dropbox_auth/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2016-2020 Moritz Schappler - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/script.module.dropbox_auth/addon.xml b/script.module.dropbox_auth/addon.xml deleted file mode 100644 index 11d5e6b88..000000000 --- a/script.module.dropbox_auth/addon.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - Dropbox Auth API - Provide Dropbox Authentification - all - MIT - https://github.com/SchapplM/xbmc-addon-dropboxauth - - diff --git a/script.module.dropbox_auth/authenticate.py b/script.module.dropbox_auth/authenticate.py deleted file mode 100644 index f9e3c4f3a..000000000 --- a/script.module.dropbox_auth/authenticate.py +++ /dev/null @@ -1,121 +0,0 @@ -# This script should be called in the settings dialog of other addons that use the dropbox API. -# example (from settings.xml): -# -# -# This script authorizes with dropbox by using the given App key (has to be requested at dropbox) and the app secret. -# The registration token will be written back to the settings of the calling addon (with given id and settings name) - -import xbmc, xbmcgui, xbmcaddon, xbmcvfs -import os, sys -from dropbox import DropboxOAuth2FlowNoRedirect - -# PIL needed for QR code generation (source code from qr-code.py from service.linuxwhatelse.notify) -''' -The QR-Code module used need the PIL (Python Image Library) to draw -the image. On some platforms (like android) PIL isn't available so -we check for the availability of this module and in case it is not -available we show a notification informing the user about it -''' -try: - import PIL - PIL_AVAILABLE = True -except ImportError: - PIL_AVAILABLE = False -import qrcode - -import resources.utils as utils - - -# parse input arguments -if len(sys.argv) == 5:# - ADDON_TARGET_ID = sys.argv[1] # id of the calling addon - SETTINGNAME_TARGET = sys.argv[2] # name of the settings field where the output is written to - DROPBOX_APP_KEY = sys.argv[3] - DROPBOX_APP_SECRET = sys.argv[4] -else: - utils.log('expecting 5 input arguments for Target Addon Id, Setting Name, Dropbox App key and secret. Received %d:' % len(sys.argv), xbmc.LOGERROR) - utils.log(str(sys.argv), xbmc.LOGINFO) - utils.showNotification(utils.getString(32102), utils.getString(32202)) - sys.exit(1) - - -# Define a class for the Dialog to show the URL -class MyClass(xbmcgui.WindowDialog): - # Opening a xbmcgui.Window does not work, since the open settings dialog prevents this (http://forum.kodi.tv/showthread.php?tid=262100&pid=2263074#pid2263074) - # Use xbmcgui.WindowDiaolog to be able to show the QR-code image - def __init__(self, authorize_url): - - # save window resolution to arrange the text fields an QR code - screenx = self.getWidth() - screeny = self.getHeight() - utils.log('Screen resolution: %dx%d' % (screenx, screeny), xbmc.LOGDEBUG) - # Show Dialog with Dropbox Authorization URL - - res_qr_code = [0,0] # resolution of the QR-code image. - # Show QR-Code Dropbox Authorization URL (source code from qr-code.py from service.linuxwhatelse.notify) - if PIL_AVAILABLE: - tmp_dir = os.path.join(utils.data_dir()) # tmp_dir has to exist - tmp_file = os.path.join(tmp_dir, 'dropbox-auth-qr-code.png') - # Create the QR-code image and save it to temp direcotry - qr = qrcode.main.QRCode(box_size=40, border=2) - qr.add_data(authorize_url) - qr.make(fit=True) - img = qr.make_image() - img.save(tmp_file) - # Show the QR-Code in Kodi - # http://www.programcreek.com/python/example/84322/xbmcgui.getCurrentWindowId - utils.log('Add control image with %dx%d at (%d,%d)' % (screeny/2, screeny/2, 100, 100), xbmc.LOGDEBUG) - res_qr_code = [int(screeny/4), int(screeny/4)] # TODO: the image is displayed bigger than the desired size. Find out why. - image = xbmcgui.ControlImage(100, 100, res_qr_code[0], res_qr_code[1], tmp_file) - self.addControl(image) - else: - # The PIL module isn't available so we inform the user about it - utils.showNotification(utils.getString(32102), utils.getString(32201)) - - # Print the Information text below the QR code - self.addControl(xbmcgui.ControlLabel(x=100, y=(100+res_qr_code[1]+ 50), width=screenx, height=25, label=utils.getString(32704), textColor='0xFFFFFFFF')) - self.addControl(xbmcgui.ControlLabel(x=100, y=(100+res_qr_code[1]+100), width=screenx, height=25, label=authorize_url, textColor='0xFFFFFFFF')) - self.addControl(xbmcgui.ControlLabel(x=100, y=(100+res_qr_code[1]+150), width=screenx, height=25, label=utils.getString(32705), textColor='0xFFFFFFFF')) - - # Also print the text to the log file for manual use - utils.log('Open the following URL: %s' % authorize_url) - - # this shows the window on the screen - self.show() - - def onAction(self, action): - # the window will be closed with any key - self.close() - - -utils.log('Starting Dropbox authentification with key %s and secret %s' % (DROPBOX_APP_KEY, DROPBOX_APP_SECRET), xbmc.LOGDEBUG) -# start dropbox authentification -flow = DropboxOAuth2FlowNoRedirect(DROPBOX_APP_KEY, DROPBOX_APP_SECRET) -authorize_url = flow.start() -# display URL -mydisplay = MyClass(authorize_url) -mydisplay.doModal() -del mydisplay - -# Open dialog to input the confirmation code. -dialog = xbmcgui.Dialog() -code = dialog.input(utils.getString(32703), type=xbmcgui.INPUT_ALPHANUM).strip() - -if code == '': - # empty code, aborted - utils.log('Entered an empty authorization code. Abort.', xbmc.LOGDEBUG) - sys.exit(0); - -# finish authentification by sending the code to dropbox -try: - token = flow.finish(code).access_token -except Exception as e: - dialog.ok(utils.getString(32103), utils.getString(32706)+": " + str(e)) - sys.exit(1); - -# positive notification -utils.showNotification(utils.getString(32103), utils.getString(32707)) - -# return the token to the calling script. That means writing the token in the pre-defined settings field -__Addon_Target = xbmcaddon.Addon(ADDON_TARGET_ID) -__Addon_Target.setSetting(SETTINGNAME_TARGET,token) diff --git a/script.module.dropbox_auth/resources/language/resource.language.de_de/strings.po b/script.module.dropbox_auth/resources/language/resource.language.de_de/strings.po deleted file mode 100644 index 444a43b09..000000000 --- a/script.module.dropbox_auth/resources/language/resource.language.de_de/strings.po +++ /dev/null @@ -1,58 +0,0 @@ -# XBMC Media Center language file -# Addon Name: Dropbox Auth -# Addon id: script.module.dropbox_auth -# Addon version: 1.0.0 -# Addon Provider: schapplm -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: http://trac.xbmc.org/\n" -"POT-Creation-Date: 2017-11-20 22:00+0400\n" -"PO-Revision-Date: 2017-11-20 22:00+0400\n" -"Last-Translator: Moritz Schappler \n" -"Language-Team: LANGUAGE\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: de\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "32101" -msgid "Dropbox" -msgstr "Dropbox" - -msgctxt "32102" -msgid "Dropbox: Error" -msgstr "Dropbox: Fehler" - -msgctxt "32103" -msgid "Dropbox: Setup" -msgstr "Dropbox: Konfiguration" - -msgctxt "32201" -msgid "Python Image Library for QR code not available" -msgstr "Python Image Library für QR-Code-Erzeugung nicht verfügbar" - -msgctxt "32202" -msgid "Wrong input arguments" -msgstr "Falsche Anzahl an Eingabeargumenten" - -msgctxt "32703" -msgid "Dropbox authorization Code" -msgstr "Dropbox Bestätigungscode" - -msgctxt "32704" -msgid "Open the following URL by scanning the QR-Code or by typing it manually" -msgstr "Öffnen Sie die folgende URL durch Scannen des QR-Codes oder abtippen" - -msgctxt "32705" -msgid "Press any key to close the QR-Code and continue to enter your the authorization code" -msgstr "Drücken Sie eine beliebige Taste zum Schließen dieses Fensters und anschließender Eingabe des Bestätigungscodes" - -msgctxt "32706" -msgid "Unable to authorize with Dropbox" -msgstr "Autorisierung mit Dropbox nicht möglich" - -msgctxt "32707" -msgid "Dropbox successfully authorized" -msgstr "Autorisierung mit Dropbox erfolgreich abgeschlossen" diff --git a/script.module.dropbox_auth/resources/language/resource.language.en_gb/strings.po b/script.module.dropbox_auth/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index f0c075dec..000000000 --- a/script.module.dropbox_auth/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,58 +0,0 @@ -# XBMC Media Center language file -# Addon Name: Dropbox Auth -# Addon id: script.module.dropbox_auth -# Addon version: 1.0.0 -# Addon Provider: schapplm -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: http://trac.xbmc.org/\n" -"POT-Creation-Date: 2017-11-20 22:00+0400\n" -"PO-Revision-Date: 2017-11-20 22:00+0400\n" -"Last-Translator: FULL NAME \n" -"Language-Team: English\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "#32101" -msgid "Dropbox" -msgstr "" - -msgctxt "#32102" -msgid "Dropbox: Error" -msgstr "" - -msgctxt "#32103" -msgid "Dropbox: Setup" -msgstr "" - -msgctxt "#32201" -msgid "Python Image Library for QR code not available" -msgstr "" - -msgctxt "#32202" -msgid "Wrong input arguments" -msgstr "" - -msgctxt "#32703" -msgid "Dropbox authorization Code" -msgstr "" - -msgctxt "#32704" -msgid "Open the following URL by scanning the QR-Code or by typing it manually" -msgstr "" - -msgctxt "#32705" -msgid "Press any key to close the QR-Code and continue to enter your the authorization code" -msgstr "" - -msgctxt "#32706" -msgid "Unable to authorize with Dropbox" -msgstr "" - -msgctxt "#32707" -msgid "Dropbox successfully authorized" -msgstr "" diff --git a/script.module.dropbox_auth/resources/utils.py b/script.module.dropbox_auth/resources/utils.py deleted file mode 100644 index 03f82e057..000000000 --- a/script.module.dropbox_auth/resources/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -This file contains additional utility functions -""" -import xbmc, xbmcgui, xbmcvfs, xbmcaddon -import os -import sys - -__addon_id__= u'script.module.dropbox_auth' -__Addon = xbmcaddon.Addon(__addon_id__) - - -def data_dir(): - """"get user data directory of this addon. - according to http://wiki.xbmc.org/index.php?title=Add-on_Rules#Requirements_for_scripts_and_plugins - """ - __datapath__ = xbmcvfs.translatePath( __Addon.getAddonInfo('profile') ) - if not xbmcvfs.exists(__datapath__): - xbmcvfs.mkdir(__datapath__) - return __datapath__ - -def addon_dir(): - """"get source directory of this addon. - according to http://wiki.xbmc.org/index.php?title=Add-on_Rules#Requirements_for_scripts_and_plugins - """ - return __Addon.getAddonInfo('path') - -def log(message,loglevel=xbmc.LOGDEBUG): - """"save message to kodi.log. - - Args: - message: has to be unicode, http://wiki.xbmc.org/index.php?title=Add-on_unicode_paths#Logging - loglevel: xbmc.LOGDEBUG, xbmc.LOGINFO, xbmc.LOGWARNING, xbmc.LOGERROR, xbmc.LOGFATAL - """ - xbmc.log(__addon_id__ + u": " + message, level=loglevel) - - -def showNotification(title,message, showtime=4000): - """Show Notification - - Args: - title: has to be unicode - message: has to be unicode - time: Time that the message is beeing displayed - """ - __addoniconpath__ = os.path.join(addon_dir(), "icon.png") - log(u'Notification. %s: %s' % (title, message)) - xbmcgui.Dialog().notification(title, message, __addoniconpath__, showtime) - - -def getString(string_id): - # return a localized string from resources/language/*.po - # The returned string is unicode - return __Addon.getLocalizedString(string_id) diff --git a/script.module.infotagger/README.md b/script.module.infotagger/README.md deleted file mode 100644 index 3ee0e00ee..000000000 --- a/script.module.infotagger/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# script.module.infotagger -The InfoTagger module is a wrapper for new Nexus InfoTagVideo ListItem methods to maintain backwards compatibility with old ListItem.setInfo() style methods - - -Usage - -Import as a dependency in `addon.xml` -```xml - - - - ``` - -When making your ListItem - - ```python - from infotagger.listitem import ListItemInfoTag - -# Make your listitem as normal -li = xbmcgui.ListItem() - -# Pass listitem to the infotagger module and specify tag type -info_tag = ListItemInfoTag(li, 'video') - -# li.setInfo(infolabels) -info_tag.set_info(infolabels) - -# li.setUniqueIDs(unique_ids) -info_tag.set_unique_ids(unique_ids) - -# li.setCast(cast) -info_tag.set_cast(cast) - -# li.addStreamInfo('video', videostream_values) -info_tag.add_stream_info('video', videostream_values) - ``` - - -Optional alternative for setting all video/audio/subtitle streams using a single dictionary (e.g. as returned by JSON RPC). - -```python -""" -stream_details = { - 'video': [{videostream_1_values}, {videostream_2_values} ...], - 'audio': [{audiostream_1_values}, {audiostream_2_values} ...], - 'subtitle': [{subtitlestream_1_values}, {subtitlestream_2_values} ...]} -""" -info_tag.set_stream_details(stream_details) -``` - diff --git a/script.module.infotagger/addon.xml b/script.module.infotagger/addon.xml deleted file mode 100644 index 269723139..000000000 --- a/script.module.infotagger/addon.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - all - Python wrapper for setInfo on Nexus InfoTags - Python wrapper for setInfo on Nexus InfoTags - GPL-3.0-or-later - https://github.com/jurialmunkey/script.module.infotagger - https://github.com/jurialmunkey/script.module.infotagger - - icon.png - - - - diff --git a/script.module.infotagger/icon.png b/script.module.infotagger/icon.png deleted file mode 100644 index 3f5773545..000000000 Binary files a/script.module.infotagger/icon.png and /dev/null differ diff --git a/script.module.infotagger/resources/modules/infotagger/listitem.py b/script.module.infotagger/resources/modules/infotagger/listitem.py deleted file mode 100644 index 8adee8f2b..000000000 --- a/script.module.infotagger/resources/modules/infotagger/listitem.py +++ /dev/null @@ -1,262 +0,0 @@ -# -*- coding: utf-8 -*- -# Module: default -# Author: jurialmunkey -# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html -from xbmc import Actor, VideoStreamDetail, AudioStreamDetail, SubtitleStreamDetail, LOGINFO -from xbmc import log as kodi_log - - -def set_info_tag( - listitem, infolabels: dict, - tag_type: str = 'video', type_check: bool = False, - old_method_keys: tuple = ('size', 'count', 'date', )): - - if old_method_keys: - il = {k: infolabels[k] for k in old_method_keys if k in infolabels} - listitem.setInfo(tag_type, il) - - info_tag = ListItemInfoTag(listitem, tag_type, type_check) - info_tag.set_info(infolabels) - return info_tag - - -def ListItemInfoTag(listitem, tag_type: str = 'video', type_check=False): - """ - Pass xbmcgui.ListItem() to listitem with tag_type to the library type normally in li.setInfo(type=) - Optional set type_check= - - False: (default) - - Slightly increases performance by avoiding additional internal type checks - - Relys on Kodi Python API raising a TypeError to determine when to force type conversion - - Kodi creates EXCEPTION log spam when infolabels require type conversion - - True: - - Slightly descreases performance by requiring additional internal type checks - - Uses internal isinstance type check to determine when to force type conversion - - Prevents Kodi EXCEPTION log spam when infolabels require type conversion - """ - factory_routes = { - 'video': _ListItemInfoTagVideo, - 'music': _ListItemInfoTagMusic, - 'game': _ListItemInfoTagGame - } - - return factory_routes[tag_type](listitem, type_check=type_check) - - -class _ListItemInfoTag(): - def __init__(self, listitem, type_check=False): - self._listitem = listitem - self._info_tag = getattr(self._listitem, self._tag_gttr)() - self._type_chk = type_check - - def set_info(self, infolabels: dict): - """ Wrapper for compatibility with Matrix ListItem.setInfo() method """ - for k, v in infolabels.items(): - if v is None: - continue - - try: - _tag_attr = self._tag_attr[k] - - except KeyError: - log_msg = f'[script.module.infotagger] set_info:\nKeyError: {k}' - kodi_log(log_msg, level=LOGINFO) - continue - - try: - func = getattr(self._info_tag, _tag_attr['attr']) - if self._type_chk and not isinstance(v, _tag_attr['classinfo']): - raise TypeError - func(v) - - except KeyError: - if _tag_attr.get('skip'): - continue - - if 'route' in _tag_attr: - getattr(self, _tag_attr['route'])(v, infolabels) - continue - - log_msg = _tag_attr.get('log_msg') or '' - log_msg = f'[script.module.infotagger] set_info:\nKeyError: {log_msg}' - kodi_log(log_msg, level=LOGINFO) - continue - - except TypeError: - func(_tag_attr['convert'](v)) # Attempt to force conversion to correct type - - -class _ListItemInfoTagVideo(_ListItemInfoTag): - _tag_gttr = 'getVideoInfoTag' - _tag_attr = { - 'size': {'skip': True}, # Currently no infoTag setter for this property - 'count': {'skip': True}, # Currently no infoTag setter for this property - 'date': {'attr': 'setDateAdded', 'convert': str, 'classinfo': str}, # Unsure if this is the correct place to route this generic value - 'genre': {'attr': 'setGenres', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'country': {'attr': 'setCountries', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'year': {'attr': 'setYear', 'convert': int, 'classinfo': int}, - 'episode': {'attr': 'setEpisode', 'convert': int, 'classinfo': int}, - 'season': {'attr': 'setSeason', 'convert': int, 'classinfo': int}, - 'sortepisode': {'attr': 'setSortEpisode', 'convert': int, 'classinfo': int}, - 'sortseason': {'attr': 'setSortSeason', 'convert': int, 'classinfo': int}, - 'episodeguide': {'attr': 'setEpisodeGuide', 'convert': str, 'classinfo': str}, - 'showlink': {'attr': 'setShowLinks', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'top250': {'attr': 'setTop250', 'convert': int, 'classinfo': int}, - 'setid': {'attr': 'setSetId', 'convert': int, 'classinfo': int}, - 'tracknumber': {'attr': 'setTrackNumber', 'convert': int, 'classinfo': int}, - 'rating': {'attr': 'setRating', 'convert': float, 'classinfo': float}, - 'userrating': {'attr': 'setUserRating', 'convert': int, 'classinfo': int}, - 'watched': {'skip': True}, # Evaluated internally in Nexus based on playcount so skip - 'playcount': {'attr': 'setPlaycount', 'convert': int, 'classinfo': int}, - 'overlay': {'skip': True}, # Evaluated internally in Nexus based on playcount so skip - 'cast': {'route': 'set_info_cast'}, - 'castandrole': {'route': 'set_info_cast'}, - 'director': {'attr': 'setDirectors', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'mpaa': {'attr': 'setMpaa', 'convert': str, 'classinfo': str}, - 'plot': {'attr': 'setPlot', 'convert': str, 'classinfo': str}, - 'plotoutline': {'attr': 'setPlotOutline', 'convert': str, 'classinfo': str}, - 'title': {'attr': 'setTitle', 'convert': str, 'classinfo': str}, - 'originaltitle': {'attr': 'setOriginalTitle', 'convert': str, 'classinfo': str}, - 'sorttitle': {'attr': 'setSortTitle', 'convert': str, 'classinfo': str}, - 'duration': {'attr': 'setDuration', 'convert': int, 'classinfo': int}, - 'studio': {'attr': 'setStudios', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'tagline': {'attr': 'setTagLine', 'convert': str, 'classinfo': str}, - 'writer': {'attr': 'setWriters', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'tvshowtitle': {'attr': 'setTvShowTitle', 'convert': str, 'classinfo': str}, - 'premiered': {'attr': 'setPremiered', 'convert': str, 'classinfo': str}, - 'status': {'attr': 'setTvShowStatus', 'convert': str, 'classinfo': str}, - 'set': {'attr': 'setSet', 'convert': str, 'classinfo': str}, - 'setoverview': {'attr': 'setSetOverview', 'convert': str, 'classinfo': str}, - 'tag': {'attr': 'setTags', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'imdbnumber': {'attr': 'setIMDBNumber', 'convert': str, 'classinfo': str}, - 'code': {'attr': 'setProductionCode', 'convert': str, 'classinfo': str}, - 'aired': {'attr': 'setFirstAired', 'convert': str, 'classinfo': str}, - 'credits': {'attr': 'setWriters', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'lastplayed': {'attr': 'setLastPlayed', 'convert': str, 'classinfo': str}, - 'album': {'attr': 'setAlbum', 'convert': str, 'classinfo': str}, - 'artist': {'attr': 'setArtists', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'votes': {'attr': 'setVotes', 'convert': int, 'classinfo': int}, - 'path': {'attr': 'setPath', 'convert': str, 'classinfo': str}, - 'trailer': {'attr': 'setTrailer', 'convert': str, 'classinfo': str}, - 'dateadded': {'attr': 'setDateAdded', 'convert': str, 'classinfo': str}, - 'mediatype': {'attr': 'setMediaType', 'convert': str, 'classinfo': str}, - 'dbid': {'attr': 'setDbId', 'convert': int, 'classinfo': int}, - } - - def set_info_cast(self, cast: list, *args, **kwargs): - """ Wrapper to convert cast and castandrole from ListItem.setInfo() to InfoTagVideo.setCast() """ - def _set_cast_member(x, i): - if not isinstance(i, tuple): - i = (i, '',) - return {'name': f'{i[0]}', 'role': f'{i[1]}', 'order': x, 'thumbnail': ''} - - self._info_tag.setCast([Actor(**_set_cast_member(x, i)) for x, i in enumerate(cast, start=1)]) - - def set_cast(self, cast: list): - """ Wrapper for compatibility with Matrix ListItem.setCast() method """ - self._info_tag.setCast([Actor(**i) for i in cast]) - - def set_stream_details(self, stream_details: dict): - """ Wrapper for compatibility with multiple ListItem.addStreamInfo() methods in one call """ - if not stream_details: - return - - try: - for i in stream_details['video']: - try: - self._info_tag.addVideoStream(VideoStreamDetail(**i)) - except TypeError: - # TEMP BANDAID workaround for inconsistent key names prior to Nexus Beta changes - i['hdrType'] = i.pop('hdrtype', '') - i['stereoMode'] = i.pop('stereomode', '') - self._info_tag.addVideoStream(VideoStreamDetail(**i)) - except (KeyError, TypeError): - pass - - try: - for i in stream_details['audio']: - self._info_tag.addAudioStream(AudioStreamDetail(**i)) - except (KeyError, TypeError): - pass - - try: - for i in stream_details['subtitle']: - self._info_tag.addSubtitleStream(SubtitleStreamDetail(**i)) - except (KeyError, TypeError): - pass - - def add_stream_info(self, stream_type, stream_values): - """ Wrapper for compatibility with Matrix ListItem.addStreamInfo() method """ - stream_details = {'video': [], 'audio': [], 'subtitle': []} - stream_details[stream_type] = [stream_values] - self.set_stream_details(stream_details) - - def set_resume_point(self, infoproperties: dict, resume_key='ResumeTime', total_key='TotalTime', pop_keys=True): - """ Wrapper to get/pop resumetime and totaltime properties for InfoTagVideo.setResumePoint() """ - getter_func = infoproperties.pop if pop_keys else infoproperties.get - resume_time = getter_func(resume_key, None) - total_time = getter_func(total_key, None) - if resume_time and total_time: - self._info_tag.setResumePoint(resume_time, total_time) - elif resume_time: - self._info_tag.setResumePoint(resume_time) - return infoproperties - - def set_unique_ids(self, unique_ids: dict, default_id: str = None): - """ Wrapper for compatibility with Matrix ListItem.setUniqueIDs() method """ - self._info_tag.setUniqueIDs({k: f'{v}' for k, v in unique_ids.items()}, default_id) - - -class _ListItemInfoTagMusic(_ListItemInfoTag): - _tag_gttr = 'getMusicInfoTag' - _tag_attr = { - 'size': {'skip': True}, # Currently no infoTag setter for this property - 'count': {'skip': True}, # Currently no infoTag setter for this property - 'date': {'skip': True}, # Currently no infoTag setter for this property - 'tracknumber': {'attr': 'setTrack', 'convert': int, 'classinfo': int}, - 'discnumber': {'attr': 'setDisc', 'convert': int, 'classinfo': int}, - 'duration': {'attr': 'setDuration', 'convert': int, 'classinfo': int}, - 'year': {'attr': 'setYear', 'convert': int, 'classinfo': int}, - 'genre': {'attr': 'setGenres', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'album': {'attr': 'setAlbum', 'convert': str, 'classinfo': str}, - 'artist': {'attr': 'setArtist', 'convert': str, 'classinfo': str}, - 'title': {'attr': 'setTitle', 'convert': str, 'classinfo': str}, - 'rating': {'attr': 'setRating', 'convert': float, 'classinfo': float}, - 'userrating': {'attr': 'setUserRating', 'convert': int, 'classinfo': int}, - 'lyrics': {'attr': 'setLyrics', 'convert': str, 'classinfo': str}, - 'playcount': {'attr': 'setPlayCount', 'convert': int, 'classinfo': int}, - 'lastplayed': {'attr': 'setLastPlayed', 'convert': str, 'classinfo': str}, - 'mediatype': {'attr': 'setMediaType', 'convert': str, 'classinfo': str}, - 'dbid': {'route': 'set_info_music_dbid'}, - 'listeners': {'attr': 'setListeners', 'convert': int, 'classinfo': int}, - 'musicbrainztrackid': {'attr': 'setMusicBrainzTrackID', 'convert': str, 'classinfo': str}, - 'musicbrainzartistid': {'attr': 'setMusicBrainzArtistID', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'musicbrainzalbumid': {'attr': 'setMusicBrainzAlbumID', 'convert': str, 'classinfo': str}, - 'musicbrainzalbumartistid': {'attr': 'setMusicBrainzAlbumArtistID', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'comment': {'attr': 'setComment', 'convert': str, 'classinfo': str}, - 'albumartist': {'attr': 'setAlbumArtist', 'convert': str, 'classinfo': str}, # Not listed in setInfo docs but included for forward compatibility - } - - def set_info_music_dbid(self, dbid: int, infolabels: dict, *args, **kwargs): - """ Wrapper for InfoTagMusic.setDbId to retrieve mediatype """ - try: - mediatype = infolabels['mediatype'] - self._info_tag.setDbId(int(dbid), mediatype) - except (KeyError, TypeError): - return - - -class _ListItemInfoTagGame(_ListItemInfoTag): - _tag_gttr = 'getGameInfoTag' - _tag_attr = { - 'size': {'skip': True}, # Currently no infoTag setter for this property - 'count': {'skip': True}, # Currently no infoTag setter for this property - 'date': {'skip': True}, # Currently no infoTag setter for this property - 'title': {'attr': 'setTitle', 'convert': str, 'classinfo': str}, - 'platform': {'attr': 'setPlatform', 'convert': str, 'classinfo': str}, - 'genres': {'attr': 'setGenres', 'convert': lambda x: [x], 'classinfo': (list, tuple)}, - 'publisher': {'attr': 'setPublisher', 'convert': str, 'classinfo': str}, - 'developer': {'attr': 'setDeveloper', 'convert': str, 'classinfo': str}, - 'overview': {'attr': 'setOverview', 'convert': str, 'classinfo': str}, - 'year': {'attr': 'setYear', 'convert': int, 'classinfo': int}, - 'gameclient': {'attr': 'setGameClient', 'convert': str, 'classinfo': str}, - } diff --git a/script.module.jurialmunkey/addon.xml b/script.module.jurialmunkey/addon.xml deleted file mode 100644 index fd72c259e..000000000 --- a/script.module.jurialmunkey/addon.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - Common code required by TMDbHelper and other related jurialmunkey add-ons - GPL-3.0-or-later - https://github.com/jurialmunkey/script.module.jurialmunkey - - icon.png - - - diff --git a/script.module.jurialmunkey/icon.png b/script.module.jurialmunkey/icon.png deleted file mode 100644 index 283ed67c1..000000000 Binary files a/script.module.jurialmunkey/icon.png and /dev/null differ diff --git a/script.module.jurialmunkey/resources/language/resource.language.en_gb/strings.po b/script.module.jurialmunkey/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index fde5e2f7b..000000000 --- a/script.module.jurialmunkey/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,27 +0,0 @@ -# XBMC Media Center language file -# Addon Name: TheMovieDb Helper -# Addon id: plugin.video.themoviedb.helper -# Addon Provider: jurialmunkey -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2014-10-26 17:05+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -#: /request.py -msgctxt "#32001" -msgid "Suppressing retries for {} seconds" -msgstr "" - -#: /request.py -msgctxt "#32002" -msgid "ConnectionError: {}" -msgstr "" \ No newline at end of file diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/bcache.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/bcache.py deleted file mode 100644 index 92d975586..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/bcache.py +++ /dev/null @@ -1,89 +0,0 @@ -from jurialmunkey.plugin import format_name -from jurialmunkey.futils import get_filecache_name -from jurialmunkey.logger import kodi_try_except_internal_traceback -import jurialmunkey.scache - - -class BasicCache(): - _simplecache = jurialmunkey.scache.SimpleCache - - def __init__(self, filename=None): - self._filename = filename - self._cache = None - - @staticmethod - def kodi_traceback(exc, log_msg): - from xbmc import getLocalizedString - from jurialmunkey.logger import Logger - Logger( - log_name='[script.module.jurialmunkey]\n', - notification_head=f'Module {getLocalizedString(257)}', - notification_text=getLocalizedString(2104)).kodi_traceback(exc, log_msg) - - @kodi_try_except_internal_traceback('lib.addon.cache ret_cache') - def ret_cache(self): - if not self._cache: - self._cache = self._simplecache(filename=self._filename) - return self._cache - - @kodi_try_except_internal_traceback('lib.addon.cache get_cache') - def get_cache(self, cache_name): - self.ret_cache() - cache_name = get_filecache_name(cache_name or '') - return self._cache.get(cache_name) - - @kodi_try_except_internal_traceback('lib.addon.cache set_cache') - def set_cache(self, my_object, cache_name, cache_days=14, force=False, fallback=None): - """ set object to cache via thread """ - self._set_cache(my_object, cache_name, cache_days, force, fallback) - return my_object - - def _set_cache(self, my_object, cache_name, cache_days=14, force=False, fallback=None): - """ set object to cache """ - self.ret_cache() - cache_name = get_filecache_name(cache_name or '') - if force and (not my_object or not cache_name or not cache_days): - my_object = my_object or fallback - cache_days = force if isinstance(force, int) else cache_days - self._cache.set(cache_name, my_object, cache_days=cache_days) - - @kodi_try_except_internal_traceback('lib.addon.cache del_cache') - def del_cache(self, cache_name): - self.ret_cache() - cache_name = get_filecache_name(cache_name or '') - self._cache.set(cache_name, None, cache_days=0) - - @kodi_try_except_internal_traceback('lib.addon.cache use_cache') - def use_cache( - self, func, *args, - cache_days=14, cache_name='', cache_only=False, cache_force=False, cache_strip=[], cache_fallback=False, - cache_refresh=False, cache_combine_name=False, headers=None, - **kwargs): - """ - Simplecache takes func with args and kwargs - Returns the cached item if it exists otherwise does the function - """ - if not cache_name or cache_combine_name: - cache_name = format_name(cache_name, *args, **kwargs) - for k, v in cache_strip: - cache_name = cache_name.replace(k, v) - my_cache = self.get_cache(cache_name) if not cache_refresh else None - if my_cache: - return my_cache - if not cache_only: - if headers: - kwargs['headers'] = headers - my_object = func(*args, **kwargs) - return self.set_cache(my_object, cache_name, cache_days, force=cache_force, fallback=cache_fallback) - - -def use_simple_cache(cache_days=None): - def decorator(func): - def wrapper(self, *args, **kwargs): - kwargs['cache_days'] = cache_days or kwargs.get('cache_days', None) - kwargs['cache_combine_name'] = True - kwargs['cache_name'] = f'{func.__name__}.' - kwargs['cache_name'] = f'{self.__class__.__name__}.{kwargs["cache_name"]}' - return self._cache.use_cache(func, self, *args, **kwargs) - return wrapper - return decorator diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/dialog.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/dialog.py deleted file mode 100644 index a220e3177..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/dialog.py +++ /dev/null @@ -1,73 +0,0 @@ -import xbmcgui -from xbmc import executebuiltin - - -class ProgressDialog(object): - """ ContextManager for DialogProgressBG use in with statement """ - - def __init__(self, title='', message='', total=100, logging=1): - self.logging = logging - self._create(title, message, total) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - @staticmethod - def kodi_log(msg, level=0): - from jurialmunkey.logger import Logger - Logger('[script.module.jurialmunkey]\n').kodi_log(msg, level) - - def _create(self, title='', message='', total=100): - self._pd = xbmcgui.DialogProgressBG() - self._pd.create(title, message) - self._count = 0 - self._total = total - self._title = title - self.kodi_log([self._title, ' - 00 ', message], self.logging) - return self._pd - - def update(self, message='', count=1, total=None): - if not self._pd: - return - if total: # Reset counter if given new total - self._count = count - self._total = total - self._count += count - self._progr = (((self._count) * 100) // self._total) - self._pd.update(self._progr, message=message) if message else self._pd.update(self._progr) - self.kodi_log([self._title, ' - ', self._progr, ' ', message], self.logging) - return self._progr - - def close(self): - if not self._pd: - return - self.kodi_log([self._title, ' - Done!'], self.logging) - self._pd.close() - - -class BusyDialog(): - def __init__(self, is_enabled=True): - """ ContextManager for DialogBusy in with statement """ - if is_enabled: - executebuiltin('ActivateWindow(busydialognocancel)') - self.is_enabled = is_enabled - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - if not self.is_enabled: - return - executebuiltin('Dialog.Close(busydialognocancel)') - - -def busy_decorator(func): - def wrapper(*args, **kwargs): - """ Decorator for wrappingBusyDialog around a function """ - with BusyDialog(): - response = func(*args, **kwargs) - return response - return wrapper diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/futils.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/futils.py deleted file mode 100644 index 4cbed1a78..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/futils.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -# Module: default -# Author: jurialmunkey -# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html -import xbmc -import xbmcvfs - -ADDONDATA = 'special://profile/addon_data/script.module.jurialmunkey/' -ALPHANUM_CHARS = "-_.() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -INVALID_FILECHARS = "\\/\"\'<>:|?*" - - -class FileUtils(): - addondata = ADDONDATA - - def get_write_path(self, folder, join_addon_data=True, make_dir=True): - if join_addon_data: - folder = f'{self.addondata}{folder}/' - main_dir = xbmcvfs.validatePath(xbmcvfs.translatePath(folder)) - if make_dir and not xbmcvfs.exists(main_dir): - try: # Try makedir to avoid race conditions - xbmcvfs.mkdirs(main_dir) - except FileExistsError: - pass - return main_dir - - def get_file_path(self, folder, filename, join_addon_data=True, make_dir=True): - return validate_join(self.get_write_path(folder, join_addon_data, make_dir), filename) - - -def json_loads(obj): - import json - - def json_int_keys(ordered_pairs): - result = {} - for key, value in ordered_pairs: - try: - key = int(key) - except ValueError: - pass - result[key] = value - return result - try: - return json.loads(obj, object_pairs_hook=json_int_keys) - except json.JSONDecodeError: - return - - -def json_dumps(obj, separators=(',', ':')): - from json import dumps - return dumps(obj, separators=separators) - - -def validate_join(folder, filename): - path = '/'.join([folder, filename]) - return xbmcvfs.validatePath(xbmcvfs.translatePath(path)) - - -def validify_filename(filename, alphanum=False): - import unicodedata - filename = unicodedata.normalize('NFD', filename) - filename = u''.join([c for c in filename if (not alphanum or c in ALPHANUM_CHARS) and c not in INVALID_FILECHARS]) - return filename.strip('.') - - -def get_filecache_name(cache_name, alphanum=False): - cache_name = cache_name or '' - cache_name = cache_name.replace('\\', '_').replace('/', '_').replace('.', '_').replace('?', '_').replace('&', '_').replace('=', '_').replace('__', '_') - return validify_filename(cache_name, alphanum=alphanum).rstrip('_') - - -def make_hash(content): - import hashlib - return hashlib.md5(content.encode('utf-8')).hexdigest() - - -def check_hash(hashname, hashvalue=None): - last_version = xbmc.getInfoLabel('Skin.String({})'.format(hashname)) - if not last_version: - return hashvalue - if hashvalue != last_version: - return hashvalue - - -def load_filecontent(filename=None): - try: - vfs_file = xbmcvfs.File(filename) - content = vfs_file.read() - finally: - vfs_file.close() - return content - - -def write_file(filepath=None, content=None): - if not filepath: - return - f = xbmcvfs.File(filepath, 'w') - f.write(content) - f.close() - - -def write_skinfile(filename=None, folders=None, content=None, hashvalue=None, hashname=None, checksum=None): - if not filename or not folders or not content: - return - - for folder in folders: - write_file(filepath='special://skin/{}/{}'.format(folder, filename), content=content) - - if hashvalue and hashname: - xbmc.executebuiltin('Skin.SetString({},{})'.format(hashname, hashvalue)) - - if checksum: - xbmc.executebuiltin('Skin.SetString({},{})'.format(checksum, make_hash(content))) diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/logger.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/logger.py deleted file mode 100644 index c4b03019d..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/logger.py +++ /dev/null @@ -1,150 +0,0 @@ -import xbmc -from timeit import default_timer as timer - - -def kodi_try_except_internal_traceback(log_msg, exception_type=Exception): - """ Decorator to catch exceptions and notify error for uninterruptable services """ - def decorator(func): - def wrapper(self, *args, **kwargs): - try: - return func(self, *args, **kwargs) - except exception_type as exc: - self.kodi_traceback(exc, log_msg) - return wrapper - return decorator - - -class Logger(): - def __init__( - self, - log_name: str = '', - notification_head: str = '', - notification_text: str = '', - debug_logging: bool = False): - self._addon_logname = log_name - self._debug_logging = debug_logging - self._notification_head = notification_head - self._notification_text = notification_text - - def kodi_log(self, value, level=0): - try: - if isinstance(value, list): - value = ''.join(map(str, value)) - if isinstance(value, bytes): - value = value.decode('utf-8') - logvalue = f'{self._addon_logname}{value}' - if level == 2 and self._debug_logging: - xbmc.log(logvalue, level=xbmc.LOGINFO) - elif level == 1: - xbmc.log(logvalue, level=xbmc.LOGINFO) - else: - xbmc.log(logvalue, level=xbmc.LOGDEBUG) - except Exception as exc: - xbmc.log(f'Logging Error: {exc}', level=xbmc.LOGINFO) - - def kodi_traceback(self, exception, log_msg=None, log_level=1, notification=True): - """ Method for logging caught exceptions and notifying user """ - if notification: - from xbmcgui import Dialog - Dialog().notification(self._notification_head, self._notification_text) - msg = f'Error Type: {type(exception).__name__}\nError Contents: {exception.args!r}' - msg = [log_msg, '\n', msg, '\n'] if log_msg else [msg, '\n'] - try: - import traceback - self.kodi_log(msg + traceback.format_tb(exception.__traceback__), log_level) - except Exception as exc: - self.kodi_log(f'ERROR WITH TRACEBACK!\n{exc}\n{msg}', log_level) - - def kodi_try_except(self, log_msg, exception_type=Exception): - """ Decorator to catch exceptions and notify error for uninterruptable services """ - def decorator(func): - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except exception_type as exc: - self.kodi_traceback(exc, log_msg) - return wrapper - return decorator - - def log_timer_report(self, timer_lists, paramstring): - _threaded = [ - 'item_api', 'item_tmdb', 'item_ftv', 'item_map', 'item_cache', - 'item_set', 'item_get', 'item_getx', 'item_non', 'item_nonx', 'item_art', - 'item_abc', 'item_xyz'] - total_log = timer_lists.pop('total', 0) - timer_log = ['DIRECTORY TIMER REPORT\n', paramstring, '\n'] - timer_log.append('------------------------------\n') - for k, v in timer_lists.items(): - if k in _threaded: - avg_time = f'{sum(v) / len(v):7.3f} sec avg | {max(v):7.3f} sec max | {len(v):3}' if v else ' None' - timer_log.append(f' - {k:12s}: {avg_time}\n') - elif k[:4] == 'item': - avg_time = f'{sum(v) / len(v):7.3f} sec avg | {sum(v):7.3f} sec all | {len(v):3}' if v else ' None' - timer_log.append(f' - {k:12s}: {avg_time}\n') - else: - tot_time = f'{sum(v) / len(v):7.3f} sec' if v else ' None' - timer_log.append(f'{k:15s}: {tot_time}\n') - timer_log.append('------------------------------\n') - tot_time = f'{sum(total_log) / len(total_log):7.3f} sec' if total_log else ' None' - timer_log.append(f'{"Total":15s}: {tot_time}\n') - for k, v in timer_lists.items(): - if v and k in _threaded: - timer_log.append(f'\n{k}:\n{" ".join([f"{i:.3f} " for i in v])}\n') - self.kodi_log(timer_log, 1) - - -class TryExceptLog(): - def __init__(self, exc_types=[Exception], log_msg=None, log_level=1, kodi_log=None): - """ ContextManager to allow exception passing and log """ - self.log_msg = log_msg - self.exc_types = exc_types - self.log_level = log_level - self.kodi_log = kodi_log or Logger().kodi_log - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - if exc_type and exc_type not in self.exc_types: - return - if self.log_level: - self.kodi_log(f'{self.log_msg or "ERROR PASSED"}: {exc_type}', self.log_level) - return True - - -class TimerList(): - def __init__(self, dict_obj, list_name, log_threshold=0.001, logging=True): - """ ContextManager for timing code blocks and storing in a list """ - self.list_obj = dict_obj.setdefault(list_name, []) - self.log_threshold = log_threshold - self.timer_a = timer() if logging else None - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - if not self.timer_a: - return - timer_z = timer() - total_time = timer_z - self.timer_a - if total_time > self.log_threshold: - self.list_obj.append(total_time) - - -class TimerFunc(): - def __init__(self, timer_name, log_threshold=0.05, inline=False, kodi_log=None): - """ ContextManager for timing code blocks and outputing to log """ - self.inline = ' ' if inline else '\n' - self.timer_name = timer_name - self.log_threshold = log_threshold - self.kodi_log = kodi_log or Logger().kodi_log - self.timer_a = timer() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - timer_z = timer() - total_time = timer_z - self.timer_a - if total_time > self.log_threshold: - self.kodi_log(f'{self.timer_name}{self.inline}{total_time:.3f} sec', 1) diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/parser.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/parser.py deleted file mode 100644 index f8ab3d3fd..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/parser.py +++ /dev/null @@ -1,188 +0,0 @@ -def try_int(string, base=None, fallback=0): - '''helper to parse int from string without erroring on empty or misformed string''' - try: - return int(string, base) if base else int(string) - except Exception: - return fallback - - -def try_float(string): - '''helper to parse float from string without erroring on empty or misformed string''' - try: - return float(string or 0) - except Exception: - return 0 - - -def try_str(value): - '''helper to stringify value''' - try: - return f'{value}' - except Exception: - return '' - - -def try_type(value, output=None): - if output == int: - return try_int(value) - if output == str: - return try_str(value) - if output == float: - return try_float(value) - - -def partition_list(iterable, pred): - """Use a predicate to partition entries into false entries and true entries - partition(range(10), is_odd) --> 0 2 4 6 8 and 1 3 5 7 9 - """ - from itertools import tee, filterfalse - t1, t2 = tee(iterable) - return filterfalse(pred, t1), filter(pred, t2) - - -def parse_paramstring(paramstring): - """ helper to assist to standardise urllib parsing """ - from urllib.parse import unquote_plus - from unicodedata import normalize - params = dict() - paramstring = paramstring.replace('&', '&') # Just in case xml string - for param in paramstring.split('&'): - if '=' not in param: - continue - k, v = param.split('=') - params[unquote_plus(k)] = normalize('NFKD', unquote_plus(v)).strip('\'').strip('"') # Normalize and decompose combined utf-8 forms such as Arabic and strip out quotes - return params - - -def get_between_strings(string, startswith='', endswith=''): - import re - exp = startswith + '(.+?)' + endswith - try: - return re.search(exp, string).group(1) - except AttributeError: - return '' - - -def reconfigure_legacy_params(**kwargs): - if 'type' in kwargs: - kwargs['tmdb_type'] = kwargs.pop('type') - # if kwargs.get('tmdb_type') in ['season', 'episode']: - # kwargs['tmdb_type'] = 'tv' - return kwargs - - -def dict_to_list(items, key): - items = items or [] - return [i[key] for i in items if i.get(key)] - - -def merge_two_dicts(x, y, reverse=False, deep=False): - xx = y or {} if reverse else x or {} - yy = x or {} if reverse else y or {} - z = xx.copy() # start with x's keys and values - if not deep: # modifies z with y's keys and values - z.update(yy) - return z - for k, v in yy.items(): - if isinstance(v, dict): - merge_two_dicts(z.setdefault(k, {}), v, reverse=reverse, deep=True) - elif v: - z[k] = v - return z - - -def merge_dicts(org, upd, skipempty=False): - source = org.copy() - for k, v in upd.items(): - if not k: - continue - if skipempty and not v: - continue - if isinstance(v, dict): - if not isinstance(source.get(k), dict): - source[k] = {} - source[k] = merge_dicts(source.get(k), v, skipempty=skipempty) - continue - source[k] = v - return source - - -def merge_two_items(base_item, item): - item = item or {} - base_item = base_item or {} - item['stream_details'] = merge_two_dicts(base_item.get('stream_details', {}), item.get('stream_details', {})) - item['params'] = merge_two_dicts(base_item.get('params', {}), item.get('params', {})) - item['infolabels'] = merge_two_dicts(base_item.get('infolabels', {}), item.get('infolabels', {})) - item['infoproperties'] = merge_two_dicts(base_item.get('infoproperties', {}), item.get('infoproperties', {})) - item['art'] = merge_two_dicts(base_item.get('art', {}), item.get('art', {})) - item['unique_ids'] = merge_two_dicts(base_item.get('unique_ids', {}), item.get('unique_ids', {})) - item['cast'] = item.get('cast') or base_item.get('cast') or [] - return item - - -def del_empty_keys(d, values: list = None): - values = values or [] - values += [None, ''] - return {k: v for k, v in d.items() if v not in values} - - -def find_dict_in_list(list_of_dicts, key, value): - """ Returns list of indexes for list of dictionaries where d.get(key) == value """ - return [list_index for list_index, dic in enumerate(list_of_dicts) if dic.get(key) == value] - - -def find_dict_list_index(list_of_dicts, key, value, default=None): - """ Returns first index for list of dictionaries where d.get(ket) == value """ - return next((list_index for list_index, dic in enumerate(list_of_dicts) if dic[key] == value), default) - - -def get_params(item, tmdb_type, tmdb_id=None, params=None, definition=None, base_tmdb_type=None, iso_country=None): - params = params or {} - tmdb_id = tmdb_id or item.get('id') - if params == -1: - return {} - definition = definition or {'info': 'details', 'tmdb_type': '{tmdb_type}', 'tmdb_id': '{tmdb_id}'} - for k, v in definition.items(): - params[k] = v.format(tmdb_type=tmdb_type, tmdb_id=tmdb_id, base_tmdb_type=base_tmdb_type, iso_country=iso_country, **item) - return del_empty_keys(params) - - -def load_in_data(byt, msk): - lmas = len(msk) - outp = bytes(c ^ msk[i % lmas] for i, c in enumerate(byt)) - return outp - - -def split_items(items, separator='/'): - if items and isinstance(items, str): - return items.split(f' {separator} ') - if not isinstance(items, list): - return [items] - return items - - -class EncodeURL(): - def __init__(self, plugin_path): - self._pluginpath = plugin_path - - def encode_url(self, path=None, **kwargs): - from urllib.parse import urlencode - path = path or self._pluginpath - paramstring = f'?{urlencode(kwargs)}' if kwargs else '' - return f'{path}{paramstring}' - - -class IterProps(): - def __init__(self, max_props: int = 10): - self._max_props = max_props - - def iter_props(self, items, property_name, infoproperties=None, func=None, **kwargs): - infoproperties = infoproperties or {} - if not items or not isinstance(items, list): - return infoproperties - for x, i in enumerate(items, start=1): - for k, v in kwargs.items(): - infoproperties[f'{property_name}.{x}.{k}'] = func(i.get(v)) if func else i.get(v) - if x >= self._max_props: - break - return infoproperties diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/plugin.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/plugin.py deleted file mode 100644 index afe8b67d0..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/plugin.py +++ /dev/null @@ -1,58 +0,0 @@ -import xbmc -from xbmcaddon import Addon as KodiAddon - - -class KodiPlugin(): - def __init__(self, addon_id): - self._addon_id = addon_id - self._addon = KodiAddon(addon_id) - self._addon_name = self._addon.getAddonInfo('name') - self._addon_path = self._addon.getAddonInfo('path') - self._addon_getsettingroute = { - 'bool': self._addon.getSettingBool, - 'int': self._addon.getSettingInt, - 'str': self._addon.getSettingString} - self._addon_setsettingroute = { - 'bool': self._addon.setSettingBool, - 'int': self._addon.setSettingInt, - 'str': self._addon.setSettingString} - - def get_setting(self, setting, mode='bool'): - return self._addon_getsettingroute[mode](setting) - - def set_setting(self, setting, data, mode='bool'): - return self._addon_setsettingroute[mode](setting, data) - - def get_localized(self, localize_int=0): - if localize_int < 30000 or localize_int >= 33000: - return xbmc.getLocalizedString(localize_int) - return self._addon.getLocalizedString(localize_int) - - -def format_name(cache_name, *args, **kwargs): - # Define a type whitelist to avoiding adding non-basic types like classes to cache name - permitted_types = (int, float, str, bool, bytes) - for arg in args: - if not isinstance(arg, permitted_types): - continue - cache_name = f'{cache_name}/{arg}' if cache_name else f'{arg}' - for key, value in sorted(kwargs.items()): - if not isinstance(value, permitted_types): - continue - cache_name = f'{cache_name}&{key}={value}' if cache_name else f'{key}={value}' - return cache_name - - -def format_folderpath(path, content='videos', affix='return', info=None, play='PlayMedia'): - if not path: - return - if info == 'play': - return f'{play}({path})' - if xbmc.getCondVisibility("Window.IsMedia") and xbmc.getInfoLabel("System.CurrentWindow").lower() == content: - return f'Container.Update({path})' - return f'ActivateWindow({content},{path},{affix})' - - -def set_kwargattr(obj, kwargs): - for k, v in kwargs.items(): - setattr(obj, k, v) diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/reqapi.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/reqapi.py deleted file mode 100644 index 74c774f32..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/reqapi.py +++ /dev/null @@ -1,264 +0,0 @@ -from xbmc import getCondVisibility -from xbmcgui import Dialog - -from jurialmunkey.parser import try_int -from jurialmunkey.window import get_property -from jurialmunkey.tmdate import get_timestamp, set_timestamp -from jurialmunkey.plugin import KodiPlugin -from jurialmunkey.bcache import BasicCache - -KODIPLUGIN = KodiPlugin('script.module.jurialmunkey') -get_localized = KODIPLUGIN.get_localized - -CACHE_SHORT, CACHE_MEDIUM, CACHE_LONG, CACHE_EXTENDED = 1, 7, 14, 90 - - -""" Lazyimports -from copy import copy -from json import dumps -import requests -""" - - -def translate_xml(request): - """ MiniDOM alternative to ElementTree parsing of XML to dictionary """ - - def dictify(r, root=True, parent_dict=None): - if root: - - r = r.firstChild - return {r.tagName: dictify(r, False)} - - if parent_dict is None: - parent_dict = {} - - for c in r.childNodes: - if c.nodeType == c.TEXT_NODE: - parent_dict['_text'] = c.nodeValue - continue - - child_list = parent_dict.setdefault(c.tagName, []) - child_dict = {k: v for k, v in c.attributes.items()} if c.attributes else {} - child_list.append(child_dict) - - if c.childNodes: - dictify(c, False, child_dict) - - return parent_dict - - if not request: - return - - from xml.dom.minidom import parseString - from xml.parsers.expat import ExpatError - try: - return dictify(parseString(request.text)) - except ExpatError: - return - - -def json_loads(obj): - from json import loads - return loads(obj) - - -class RequestAPI(object): - error_notification = None - _basiccache = BasicCache - - def __init__(self, req_api_url=None, req_api_key=None, req_api_name=None, timeout=None, error_notification=None): - self.req_api_url = req_api_url or '' - self.req_api_key = req_api_key or '' - self.req_api_name = req_api_name or '' - self.req_timeout_err_prop = f'TimeOutError.{self.req_api_name}' - self.req_timeout_err = 0 # Only check last timeout on timeout since we only want to suppress when multiple - self.req_connect_err_prop = f'ConnectionError.{self.req_api_name}' - self.req_connect_err = get_property(self.req_connect_err_prop, is_type=float) or 0 - self.req_500_err_prop = f'500Error.{self.req_api_name}' - self.req_500_err = get_property(self.req_500_err_prop) - self.req_500_err = json_loads(self.req_500_err) if self.req_500_err else {} - self.req_strip = [(self.req_api_url, self.req_api_name), (self.req_api_key, ''), ('is_xml=False', ''), ('is_xml=True', '')] - self.headers = None - self.timeout = timeout or 15 - self._cache = self._basiccache(filename=f'{req_api_name or "requests"}.db') - self._error_notification = error_notification or self.error_notification - self.translate_xml = translate_xml - - @staticmethod - def kodi_log(msg, level=0): - from jurialmunkey.logger import Logger - Logger('[script.module.jurialmunkey]\n').kodi_log(msg, level) - - def do_error_notification(self, log_msg, note_head, note_body): - self.kodi_log(log_msg, 1) - if not self._error_notification: - return - Dialog().notification(note_head, note_body) - - def get_api_request_json(self, request=None, postdata=None, headers=None, is_xml=False, method=None): - request = self.get_api_request(request=request, postdata=postdata, headers=headers, method=method) - if not request: - return {} - response = self.translate_xml(request) if is_xml else request.json() - request.close() - return response - - def nointernet_err(self, err, log_time=900): - # Check Kodi internet status to confirm network is down - if getCondVisibility("System.InternetState"): - return - - # Get the last error timestamp - err_prop = f'NoInternetError.{self.req_api_name}' - last_err = get_property(err_prop, is_type=float) or 0 - - # Only log error and notify user if it hasn't happened in last {log_time} seconds to avoid log/gui spam - if not get_timestamp(last_err): - self.do_error_notification( - f'ConnectionError: {get_localized(13297)}\n{err}\nSuppressing retries.', - get_localized(32002).format(self.req_api_name), get_localized(13297)) - - # Update our last error timestamp and return it - return get_property(err_prop, set_timestamp(log_time)) - - def connection_error(self, err, wait_time=30, msg_affix='', check_status=False): - self.req_connect_err = set_timestamp(wait_time) - get_property(self.req_connect_err_prop, self.req_connect_err) - - if check_status and self.nointernet_err(err): - return - - self.do_error_notification( - f'ConnectionError: {msg_affix} {err}\nSuppressing retries for {wait_time} seconds', - get_localized(32002).format(' '.join([self.req_api_name, msg_affix])), - get_localized(32001).format(f'{wait_time}')) - - def fivehundred_error(self, request, wait_time=60): - from json import dumps - self.req_500_err[request] = set_timestamp(wait_time) - get_property(self.req_500_err_prop, dumps(self.req_500_err)) - self.do_error_notification( - f'ConnectionError: {dumps(self.req_500_err)}\nSuppressing retries for {wait_time} seconds', - get_localized(32002).format(self.req_api_name), - get_localized(32001).format(f'{wait_time}')) - - def timeout_error(self, err): - """ Log timeout error - If two timeouts occur in x3 the timeout limit then set connection error - e.g. if timeout limit is 10s then two timeouts within 30s trigger connection error - """ - self.kodi_log(f'ConnectionTimeOut: {err}', 1) - self.req_timeout_err = self.req_timeout_err or get_property(self.req_timeout_err_prop, is_type=float) or 0 - if get_timestamp(self.req_timeout_err): - self.connection_error(err, msg_affix='timeout') - self.req_timeout_err = set_timestamp(self.timeout * 3) - get_property(self.req_timeout_err_prop, self.req_timeout_err) - - def get_simple_api_request(self, request=None, postdata=None, headers=None, method=None): - import requests - try: - if method == 'delete': - return requests.delete(request, headers=headers, timeout=self.timeout) - if method == 'put': - return requests.put(request, data=postdata, headers=headers, timeout=self.timeout) - if method == 'json': - return requests.post(request, json=postdata, headers=headers, timeout=self.timeout) - if postdata or method == 'post': # If pass postdata assume we want to post - return requests.post(request, data=postdata, headers=headers, timeout=self.timeout) - return requests.get(request, headers=headers, timeout=self.timeout) - except requests.exceptions.ConnectionError as errc: - self.connection_error(errc, check_status=True) - except requests.exceptions.Timeout as errt: - self.timeout_error(errt) - except Exception as err: - self.kodi_log(f'RequestError: {err}', 1) - - def get_api_request(self, request=None, postdata=None, headers=None, method=None): - """ - Make the request to the API by passing a url request string - """ - # Connection error in last minute for this api so don't keep trying - if get_timestamp(self.req_connect_err): - return - if get_timestamp(self.req_500_err.get(request)): - return - - # Get response - response = self.get_simple_api_request(request, postdata, headers, method) - if response is None or not response.status_code: - return - - # Some error checking - if not response.status_code == 200 and try_int(response.status_code) >= 400: # Error Checking - # 500 code is server error which usually indicates Trakt is down - # In this case let's set a connection error and suppress retries for a minute - if response.status_code == 500: - self.fivehundred_error(request) - # 503 code is server error which usually indicates Trakt has database maintenance - # In this case let's set a connection error and suppress retries for five minutes - elif response.status_code == 503: - self.connection_error(503, wait_time=300) - # 429 is too many requests code so suppress retries for a minute - elif response.status_code == 429: - self.connection_error(429) - # Don't write 400 Bad Request error to log - # 401 == OAuth / API key required - elif try_int(response.status_code) > 400: - log_level = 2 if try_int(response.status_code) in [404] else 1 - self.kodi_log([ - f'HTTP Error Code: {response.status_code}', - f'\nRequest: {request.replace(self.req_api_key, "") if request else None}', - f'\nPostdata: {postdata}' if postdata else '', - f'\nHeaders: {headers}' if headers else '', - f'\nResponse: {response}' if response else ''], log_level) - return - - # Return our response - return response - - def get_request_url(self, *args, **kwargs): - """ - Creates a url request string: - https://api.themoviedb.org/3/arg1/arg2?api_key=foo&kwparamkey=kwparamvalue - """ - url = '/'.join((self.req_api_url, '/'.join(map(str, (i for i in args if i is not None))))) - sep = '&' if '?' in url else '?' - if self.req_api_key: - url = sep.join((url, self.req_api_key)) - sep = '&' - if not kwargs: - return url - kws = '&'.join((f'{k}={v}' for k, v in kwargs.items() if v is not None)) - return sep.join((url, kws)) if kws else url - - def get_request_sc(self, *args, **kwargs): - """ Get API request using the short cache """ - kwargs['cache_days'] = CACHE_SHORT - return self.get_request(*args, **kwargs) - - def get_request_lc(self, *args, **kwargs): - """ Get API request using the long cache """ - kwargs['cache_days'] = CACHE_LONG - return self.get_request(*args, **kwargs) - - def get_request( - self, *args, - cache_days=0, cache_name='', cache_only=False, cache_force=False, cache_fallback=False, cache_refresh=False, - cache_combine_name=False, cache_strip=[], headers=None, postdata=None, is_xml=False, - **kwargs): - """ Get API request from cache (or online if no cached version) """ - cache_strip = self.req_strip + cache_strip - request_url = self.get_request_url(*args, **kwargs) - return self._cache.use_cache( - self.get_api_request_json, request_url, - headers=headers or self.headers, # Optional override to default headers. - postdata=postdata, # Postdata if need to POST to a RESTful API. - is_xml=is_xml, # Response needs translating from XML to dict - cache_refresh=cache_refresh, # Ignore cached timestamps and retrieve new object. - cache_days=cache_days, # Number of days to cache retrieved object if not already in cache. - cache_name=cache_name, # Affix to standard cache name. - cache_only=cache_only, # Only retrieve object from cache. - cache_force=cache_force, # Force retrieved object to be saved in cache. Use int to specify cache_days for fallback object. - cache_fallback=cache_fallback, # Object to force cache if no object retrieved. - cache_combine_name=cache_combine_name, # Combine given cache_name with auto naming via args/kwargs - cache_strip=cache_strip) # Strip out api key and url from cache name diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/scache.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/scache.py deleted file mode 100644 index 76ac1d33a..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/scache.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -import zlib -import xbmcvfs -from xbmcgui import Window -from xbmc import Monitor, sleep -from contextlib import contextmanager -from jurialmunkey.tmdate import set_timestamp -from jurialmunkey.futils import FileUtils -from jurialmunkey.futils import json_loads as data_loads -from json import dumps as data_dumps -import sqlite3 - - -FILEUTILS = FileUtils() - - -DATABASE_NAME = 'database_v6' -TIME_MINUTES = 60 -TIME_HOURS = 60 * TIME_MINUTES -TIME_DAYS = 24 * TIME_HOURS - - -class SimpleCache(object): - '''simple stateless caching system for Kodi''' - _exit = False - _auto_clean_interval = 4 * TIME_HOURS - _win = None - _busy_tasks = [] - _database = None - _memcache = False - _basefolder = '' - _fileutils = FILEUTILS - - def __init__(self, folder=None, filename=None): - '''Initialize our caching class''' - folder = folder or DATABASE_NAME - basefolder = f'{self._basefolder}{folder}' - filename = filename or 'defaultcache.db' - self._win = Window(10000) - self._monitor = Monitor() - self._db_file = self._fileutils.get_file_path(basefolder, filename, join_addon_data=basefolder == folder) - self._sc_name = f'{folder}_{filename}_simplecache' - self._queue = [] - self._re_use_con = True - self._connection = None - self.check_cleanup() - self.kodi_log("CACHE: Initialized") - - @staticmethod - def kodi_log(msg, level=0): - from jurialmunkey.logger import Logger - Logger('[script.module.jurialmunkey]\n').kodi_log(msg, level) - - def close(self): - '''tell any tasks to stop immediately (as we can be called multithreaded) and cleanup objects''' - self._exit = True - # wait for all tasks to complete - while self._busy_tasks and not self._monitor.abortRequested(): - sleep(25) - self.kodi_log(f'CACHE: Closed {self._sc_name}', 2) - - def __del__(self): - '''make sure close is called''' - if self._queue: - self.kodi_log(f'CACHE: Write {len(self._queue)} Items in Queue\n{self._sc_name}', 2) - for i in self._queue: - self._set_db_cache(*i) - self._queue = [] - self.close() - - @contextmanager - def busy_tasks(self, task_name): - self._busy_tasks.append(task_name) - try: - yield - finally: - self._busy_tasks.remove(task_name) - - def get(self, endpoint): - ''' - get object from cache and return the results - endpoint: the (unique) name of the cache object as reference - ''' - cur_time = set_timestamp(0, True) - result = self._get_mem_cache(endpoint, cur_time) # Try from memory first - return result or self._get_db_cache(endpoint, cur_time) # Fallback to checking database if not in memory - - def set(self, endpoint, data, cache_days=30): - """ set data in cache """ - with self.busy_tasks(f'set.{endpoint}'): - expires = set_timestamp(cache_days * TIME_DAYS, True) - data = data_dumps(data, separators=(',', ':')) - self._set_mem_cache(endpoint, expires, data) - self._set_db_cache(endpoint, expires, data) - - def check_cleanup(self): - '''check if cleanup is needed - public method, may be called by calling addon''' - cur_time = set_timestamp(0, True) - lastexecuted = self._win.getProperty(f'{self._sc_name}.clean.lastexecuted') - if not lastexecuted: - self._win.setProperty(f'{self._sc_name}.clean.lastexecuted', str(cur_time - self._auto_clean_interval + 600)) - elif (int(lastexecuted) + self._auto_clean_interval) < cur_time: - self._do_cleanup() - - def _get_mem_cache(self, endpoint, cur_time): - ''' - get cache data from memory cache - we use window properties because we need to be stateless - ''' - if not self._memcache: - return - - # Check expiration time - expr_endpoint = f'{self._sc_name}_expr_{endpoint}' - expr_propdata = self._win.getProperty(expr_endpoint) - if not expr_propdata or int(expr_propdata) <= cur_time: - return - - # Retrieve data - data_endpoint = f'{self._sc_name}_data_{endpoint}' - data_propdata = self._win.getProperty(data_endpoint) - if not data_propdata: - return - - return data_loads(data_propdata) - - def _set_mem_cache(self, endpoint, expires, data): - ''' - window property cache as alternative for memory cache - usefull for (stateless) plugins - ''' - if not self._memcache: - return - expr_endpoint = f'{self._sc_name}_expr_{endpoint}' - data_endpoint = f'{self._sc_name}_data_{endpoint}' - self._win.setProperty(expr_endpoint, str(expires)) - self._win.setProperty(data_endpoint, data) - - def _get_db_cache(self, endpoint, cur_time): - '''get cache data from sqllite _database''' - result = None - query = "SELECT expires, data, checksum FROM simplecache WHERE id = ? LIMIT 1" - cache_data = self._execute_sql(query, (endpoint,)) - if not cache_data: - return - cache_data = cache_data.fetchone() - if not cache_data or int(cache_data[0]) <= cur_time: - return - try: - data = str(zlib.decompress(cache_data[1]), 'utf-8') - except TypeError: - data = cache_data[1] - self._set_mem_cache(endpoint, cache_data[0], data) - result = data_loads(data) - return result - - def _set_db_cache(self, endpoint, expires, data): - ''' store cache data in _database ''' - query = "INSERT OR REPLACE INTO simplecache( id, expires, data, checksum) VALUES (?, ?, ?, ?)" - data = zlib.compress(bytes(data, 'utf-8')) - self._execute_sql(query, (endpoint, expires, data, 0)) - - def _do_delete(self): - '''perform cleanup task''' - if self._exit or self._monitor.abortRequested(): - return - - with self.busy_tasks(__name__): - cur_time = set_timestamp(0, True) - self.kodi_log(f'CACHE: Deleting {self._sc_name}...') - - self._win.setProperty(f'{self._sc_name}.cleanbusy', "busy") - - query = 'DELETE FROM simplecache' - self._execute_sql(query) - self._execute_sql("VACUUM") - - # Washup - self._win.setProperty(f'{self._sc_name}.clean.lastexecuted', str(cur_time)) - self._win.clearProperty(f'{self._sc_name}.cleanbusy') - self.kodi_log(f'CACHE: Delete {self._sc_name} done') - - def _do_cleanup(self, force=False): - '''perform cleanup task''' - if self._exit or self._monitor.abortRequested(): - return - - with self.busy_tasks(__name__): - cur_time = set_timestamp(0, True) - self.kodi_log(f"CACHE: Running cleanup...\n{self._sc_name}", 1) - if self._win.getProperty(f'{self._sc_name}.cleanbusy'): - return - self._win.setProperty(f'{self._sc_name}.cleanbusy', "busy") - - query = "SELECT id, expires FROM simplecache" - for cache_data in self._execute_sql(query).fetchall(): - cache_id = cache_data[0] - cache_expires = cache_data[1] - if self._exit or self._monitor.abortRequested(): - return - # always cleanup all memory objects on each interval - self._win.clearProperty(cache_id) - # clean up db cache object only if expired - if force or int(cache_expires) < cur_time: - query = 'DELETE FROM simplecache WHERE id = ?' - self._execute_sql(query, (cache_id,)) - self.kodi_log(f'CACHE: delete from db {cache_id}') - - # compact db - self._execute_sql("VACUUM") - - # Washup - self._win.setProperty(f'{self._sc_name}.clean.lastexecuted', str(cur_time)) - self._win.clearProperty(f'{self._sc_name}.cleanbusy') - self.kodi_log(f"CACHE: Cleanup complete...\n{self._sc_name}", 1) - - def _set_pragmas(self, connection): - if not self._connection: - connection.execute("PRAGMA synchronous=NORMAL") - connection.execute("PRAGMA journal_mode=WAL") - if self._re_use_con: - self._connection = connection - return connection - - def _get_database(self, attempts=2): - '''get reference to our sqllite _database - performs basic integrity check''' - try: - connection = self._connection or sqlite3.connect(self._db_file, timeout=5, isolation_level=None, check_same_thread=not self._re_use_con) - connection.execute('SELECT * FROM simplecache LIMIT 1') - return self._set_pragmas(connection) - except Exception: - # our _database is corrupt or doesn't exist yet, we simply try to recreate it - if xbmcvfs.exists(self._db_file): - self.kodi_log(f'CACHE: Deleting Corrupt File: {self._db_file}...', 1) - xbmcvfs.delete(self._db_file) - try: - self.kodi_log(f'CACHE: Initialising: {self._db_file}...', 1) - connection = self._connection or sqlite3.connect(self._db_file, timeout=5, isolation_level=None, check_same_thread=not self._re_use_con) - connection.execute( - """CREATE TABLE IF NOT EXISTS simplecache( - id TEXT UNIQUE, expires INTEGER, data TEXT, checksum INTEGER)""") - connection.execute("CREATE INDEX idx ON simplecache(id)") - return self._set_pragmas(connection) - except Exception as error: - self.kodi_log(f'CACHE: Exception while initializing _database: {error} ({attempts})', 1) - if attempts < 1: - return - attempts -= 1 - self._monitor.waitForAbort(1) - return self._get_database(attempts) - - def _execute_sql(self, query, data=None): - '''little wrapper around execute and executemany to just retry a db command if db is locked''' - retries = 10 - result = None - error = '' - # always use new db object because we need to be sure that data is available for other simplecache instances - with self._get_database() as _database: - while retries > 0 and not self._monitor.abortRequested(): - if self._exit: - return None - try: - if isinstance(data, list): - result = _database.executemany(query, data) - elif data: - result = _database.execute(query, data) - else: - result = _database.execute(query) - return result - except sqlite3.OperationalError as err: - error = err - try: - if "database is locked" == f'{error}': - self.kodi_log("CACHE: Locked: Retrying DB commit...", 1) - retries -= 1 - self._monitor.waitForAbort(0.5) - else: - break - except TypeError: - break - except Exception: - break - self.kodi_log(f'CACHE: _database ERROR ! -- {error}', 1) - return None diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/thread.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/thread.py deleted file mode 100644 index 7cd7ac9c0..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/thread.py +++ /dev/null @@ -1,54 +0,0 @@ -from xbmc import Monitor -from threading import Thread - - -class ParallelThread(): - thread_max = 0 # 0 is unlimited - - def __init__(self, items, func, *args, **kwargs): - """ ContextManager for running parallel threads alongside another function - with ParallelThread(items, func, *args, **kwargs) as pt: - pass - item_queue = pt.queue - item_queue[x] # to get returned items - """ - self._mon = Monitor() - thread_max = self.thread_max or len(items) - self.queue = [None] * len(items) - self._pool = [None] * thread_max - self._exit = False - for x, i in enumerate(items): - n = x - while n >= thread_max and not self._mon.abortRequested(): # Hit our thread limit so look for a spare spot in the queue - for y, j in enumerate(self._pool): - if j.is_alive(): - continue - n = y - break - if n >= thread_max: - self._mon.waitForAbort(0.025) - try: - self._pool[n] = Thread(target=self._threadwrapper, args=[x, i, func, *args], kwargs=kwargs) - self._pool[n].start() - except IndexError: - self.kodi_log(f'ParallelThread: INDEX {n} OUT OF RANGE {thread_max}', 1) - - def _threadwrapper(self, x, i, func, *args, **kwargs): - self.queue[x] = func(i, *args, **kwargs) - - @staticmethod - def kodi_log(msg, level=0): - from jurialmunkey.logger import Logger - Logger('[script.module.jurialmunkey]\n').kodi_log(msg, level) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - for i in self._pool: - if self._exit or self._mon.abortRequested(): - break - try: - i.join() - except AttributeError: # is None - pass diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/tmdate.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/tmdate.py deleted file mode 100644 index e5b414687..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/tmdate.py +++ /dev/null @@ -1,19 +0,0 @@ -import time - - -def get_timestamp(timestamp=None, set_int=False): - if not timestamp: - return - now = time.time() - tmp = timestamp - if set_int: - now = int(now) - tmp = int(tmp) - if now > tmp: - return - return timestamp - - -def set_timestamp(wait_time=60, set_int=False): - timestamp = time.time() + wait_time - return int(timestamp) if set_int else timestamp diff --git a/script.module.jurialmunkey/resources/modules/jurialmunkey/window.py b/script.module.jurialmunkey/resources/modules/jurialmunkey/window.py deleted file mode 100644 index 0e463ca91..000000000 --- a/script.module.jurialmunkey/resources/modules/jurialmunkey/window.py +++ /dev/null @@ -1,136 +0,0 @@ -from xbmc import Monitor, executebuiltin, getCondVisibility, getInfoLabel -from xbmcgui import Window, getCurrentWindowId, getCurrentWindowDialogId -from jurialmunkey.parser import try_type, try_int - - -DIALOG_ID_EXCLUDELIST = (9999, None) - - -def get_current_window(get_dialog=True): - dialog = getCurrentWindowDialogId() if get_dialog else None - return dialog if dialog not in DIALOG_ID_EXCLUDELIST else getCurrentWindowId() - - -def get_property(name, set_property=None, clear_property=False, window_id=None, prefix=None, is_type=None): - if prefix != -1: - prefix = prefix or 'TMDbHelper' - name = f'{prefix}.{name}' - if window_id == 'current': - window_id = get_current_window() - window = Window(window_id or 10000) # Fallback to home window id=10000 - ret_property = set_property or window.getProperty(name) - if clear_property: - window.clearProperty(name) - if set_property is not None: - window.setProperty(name, f'{set_property}') - return try_type(ret_property, is_type or str) - - -def _property_is_value(name, value): - if not value and not get_property(name): - return True - if value and get_property(name) == value: - return True - return False - - -def wait_for_property(name, value=None, set_property=False, poll=1, timeout=10): - """ - Waits until property matches value. None value waits for property to be cleared. - Will set property to value if set_property flag is set. None value clears property. - Returns True when successful. - """ - xbmc_monitor = Monitor() - if set_property: - get_property(name, value) if value else get_property(name, clear_property=True) - while ( - not xbmc_monitor.abortRequested() and timeout > 0 - and not _property_is_value(name, value)): - xbmc_monitor.waitForAbort(poll) - timeout -= poll - del xbmc_monitor - if timeout > 0: - return True - - -def is_visible(window_id): - return getCondVisibility(f'Window.IsVisible({window_id})') - - -def close(window_id): - return executebuiltin(f'Dialog.Close({window_id})') - - -def activate(window_id): - return executebuiltin(f'ActivateWindow({window_id})') - - -def _is_base_active(window_id): - if window_id and not is_visible(window_id): - return False - return True - - -def _is_updating(container_id): - is_updating = getCondVisibility(f"Container({container_id}).IsUpdating") - is_numitems = try_int(getInfoLabel(f"Container({container_id}).NumItems")) - if is_updating or not is_numitems: - return True - - -def _is_inactive(window_id, invert=False): - if is_visible(window_id): - return True if invert else False - return True if not invert else False - - -def wait_until_active(window_id, instance_id=None, poll=1, timeout=30, invert=False): - """ - Wait for window ID to open (or to close if invert set to True). Returns window_id if successful. - Pass instance_id if there is also a base window that needs to be open underneath - """ - xbmc_monitor = Monitor() - while ( - not xbmc_monitor.abortRequested() and timeout > 0 - and _is_inactive(window_id, invert) - and _is_base_active(instance_id)): - xbmc_monitor.waitForAbort(poll) - timeout -= poll - del xbmc_monitor - if timeout > 0 and _is_base_active(instance_id): - return window_id - - -def wait_until_updated(container_id=9999, instance_id=None, poll=1, timeout=60): - """ - Wait for container to update. Returns container_id if successful - Pass instance_id if there is also a base window that needs to be open underneath - """ - xbmc_monitor = Monitor() - while ( - not xbmc_monitor.abortRequested() and timeout > 0 - and _is_updating(container_id) - and _is_base_active(instance_id)): - xbmc_monitor.waitForAbort(poll) - timeout -= poll - del xbmc_monitor - if timeout > 0 and _is_base_active(instance_id): - return container_id - - -class WindowProperty(): - def __init__(self, *args): - """ ContextManager for setting a WindowProperty over duration """ - self.property_pairs = args - - for k, v in self.property_pairs: - if not k or not v: - continue - get_property(k, set_property=v) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - for k, v in self.property_pairs: - get_property(k, clear_property=True) diff --git a/script.module.protobuf/.gitignore b/script.module.protobuf/.gitignore deleted file mode 100644 index f23b89082..000000000 --- a/script.module.protobuf/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -__pycache__ -*.egg-info/ -*.dist-info/ -*.so -.task/ -.idea/ diff --git a/script.module.protobuf/LICENSE b/script.module.protobuf/LICENSE deleted file mode 100644 index f028c8232..000000000 --- a/script.module.protobuf/LICENSE +++ /dev/null @@ -1,42 +0,0 @@ -This license applies to all parts of Protocol Buffers except the following: - - - Atomicops support for generic gcc, located in - src/google/protobuf/stubs/atomicops_internals_generic_gcc.h. - This file is copyrighted by Red Hat Inc. - - - Atomicops support for AIX/POWER, located in - src/google/protobuf/stubs/atomicops_internals_power.h. - This file is copyrighted by Bloomberg Finance LP. - -Copyright 2014, Google Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Code generated by the Protocol Buffer compiler is owned by the owner -of the input file used when generating it. This code is not -standalone and requires a support library to be linked with it. This -support library is itself covered by the above license. diff --git a/script.module.protobuf/README.md b/script.module.protobuf/README.md deleted file mode 100644 index 67d2da147..000000000 --- a/script.module.protobuf/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# script.module.protobuf - -Kodi package of the Google Protobuf library. - -For more information view the Protobuf documentation at: https://developers.google.com/protocol-buffers/ -Or the source at: https://github.com/google/protobuf - -In case any issues are found, feel free to open an issue or pull request at -https://github.com/sanzoghenzo/kodi-protobuf diff --git a/script.module.protobuf/Taskfile.yml b/script.module.protobuf/Taskfile.yml deleted file mode 100644 index b51588d5d..000000000 --- a/script.module.protobuf/Taskfile.yml +++ /dev/null @@ -1,57 +0,0 @@ -version: "3" - -vars: - # TODO: read name and version from addon.xml - PACKAGE_NAME: protobuf - VERSION: 4.23.2 - DIST_PATH: "resources/lib" - -tasks: - install:package: - label: 'install:{{.PACKAGE_NAME}}' - vars: - PACKAGE_NAME: '{{.PACKAGE_NAME}}' - COMMAND_NAME: '{{default .PACKAGE_NAME .COMMAND_NAME}}' - cmds: - - "pip install {{.PACKAGE_NAME}}" - status: - - 'command -v {{.COMMAND_NAME}}' - - install:addon-check: - cmds: - - task: install:package - vars: - PACKAGE_NAME: kodi-addon-checker - - clean: - cmds: - - "rm -rf {{.DIST_PATH}}" - - download: - cmds: - - "mkdir -p {{.DIST_PATH}}" - - "pip install --ignore-installed --target {{.DIST_PATH}} {{.PACKAGE_NAME}}=={{.VERSION}}" - generates: - - "{{.DIST_PATH}}/**/*.py" - - fix-mode: - cmds: - - "find {{.DIST_PATH}} -type f -print0 | xargs -0 chmod 644" - - dist: - deps: - - download - - fix-mode - cmds: - - "touch {{.DIST_PATH}}/google/__init__.py" - - "rm -rf {{.DIST_PATH}}/*.dist-info" - - lint: - deps: - - install:addon-check - cmds: - - kodi-addon-checker . --branch nexus - - default: - - task: dist - - task: lint diff --git a/script.module.protobuf/addon.xml b/script.module.protobuf/addon.xml deleted file mode 100644 index c13d2b829..000000000 --- a/script.module.protobuf/addon.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - Protocol Buffers (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data. - Packed for KODI/XBMC from https://github.com/google/protobuf - https://github.com/google/protobuf/blob/master/LICENSE - all - https://protobuf.dev - https://github.com/sanzoghenzo/kodi-protobuf - andrea.ghensi@gmail.com - - resources/icon.png - - - diff --git a/script.module.protobuf/resources/icon.png b/script.module.protobuf/resources/icon.png deleted file mode 100644 index ca4c53adb..000000000 Binary files a/script.module.protobuf/resources/icon.png and /dev/null differ diff --git a/script.module.protobuf/resources/lib/google/protobuf/__init__.py b/script.module.protobuf/resources/lib/google/protobuf/__init__.py deleted file mode 100644 index 9b6e1737b..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Copyright 2007 Google Inc. All Rights Reserved. - -__version__ = '4.23.2' diff --git a/script.module.protobuf/resources/lib/google/protobuf/any_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/any_pb2.py deleted file mode 100644 index b0bfaf635..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/any_pb2.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/any.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/any.proto\x12\x0fgoogle.protobuf\"6\n\x03\x41ny\x12\x19\n\x08type_url\x18\x01 \x01(\tR\x07typeUrl\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05valueBv\n\x13\x63om.google.protobufB\x08\x41nyProtoP\x01Z,google.golang.org/protobuf/types/known/anypb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010AnyProtoP\001Z,google.golang.org/protobuf/types/known/anypb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_ANY']._serialized_start=46 - _globals['_ANY']._serialized_end=100 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/api_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/api_pb2.py deleted file mode 100644 index 4276c9422..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/api_pb2.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/api.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 -from google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/api.proto\x12\x0fgoogle.protobuf\x1a$google/protobuf/source_context.proto\x1a\x1agoogle/protobuf/type.proto\"\xc1\x02\n\x03\x41pi\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x31\n\x07methods\x18\x02 \x03(\x0b\x32\x17.google.protobuf.MethodR\x07methods\x12\x31\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x18\n\x07version\x18\x04 \x01(\tR\x07version\x12\x45\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContextR\rsourceContext\x12.\n\x06mixins\x18\x06 \x03(\x0b\x32\x16.google.protobuf.MixinR\x06mixins\x12/\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\"\xb2\x02\n\x06Method\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\x10request_type_url\x18\x02 \x01(\tR\x0erequestTypeUrl\x12+\n\x11request_streaming\x18\x03 \x01(\x08R\x10requestStreaming\x12*\n\x11response_type_url\x18\x04 \x01(\tR\x0fresponseTypeUrl\x12-\n\x12response_streaming\x18\x05 \x01(\x08R\x11responseStreaming\x12\x31\n\x07options\x18\x06 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12/\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\"/\n\x05Mixin\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n\x04root\x18\x02 \x01(\tR\x04rootBv\n\x13\x63om.google.protobufB\x08\x41piProtoP\x01Z,google.golang.org/protobuf/types/known/apipb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010ApiProtoP\001Z,google.golang.org/protobuf/types/known/apipb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_API']._serialized_start=113 - _globals['_API']._serialized_end=434 - _globals['_METHOD']._serialized_start=437 - _globals['_METHOD']._serialized_end=743 - _globals['_MIXIN']._serialized_start=745 - _globals['_MIXIN']._serialized_end=792 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/compiler/__init__.py b/script.module.protobuf/resources/lib/google/protobuf/compiler/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.module.protobuf/resources/lib/google/protobuf/compiler/plugin_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/compiler/plugin_pb2.py deleted file mode 100644 index 3e3a36de6..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/compiler/plugin_pb2.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/compiler/plugin.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%google/protobuf/compiler/plugin.proto\x12\x18google.protobuf.compiler\x1a google/protobuf/descriptor.proto\"c\n\x07Version\x12\x14\n\x05major\x18\x01 \x01(\x05R\x05major\x12\x14\n\x05minor\x18\x02 \x01(\x05R\x05minor\x12\x14\n\x05patch\x18\x03 \x01(\x05R\x05patch\x12\x16\n\x06suffix\x18\x04 \x01(\tR\x06suffix\"\xf1\x01\n\x14\x43odeGeneratorRequest\x12(\n\x10\x66ile_to_generate\x18\x01 \x03(\tR\x0e\x66ileToGenerate\x12\x1c\n\tparameter\x18\x02 \x01(\tR\tparameter\x12\x43\n\nproto_file\x18\x0f \x03(\x0b\x32$.google.protobuf.FileDescriptorProtoR\tprotoFile\x12L\n\x10\x63ompiler_version\x18\x03 \x01(\x0b\x32!.google.protobuf.compiler.VersionR\x0f\x63ompilerVersion\"\x94\x03\n\x15\x43odeGeneratorResponse\x12\x14\n\x05\x65rror\x18\x01 \x01(\tR\x05\x65rror\x12-\n\x12supported_features\x18\x02 \x01(\x04R\x11supportedFeatures\x12H\n\x04\x66ile\x18\x0f \x03(\x0b\x32\x34.google.protobuf.compiler.CodeGeneratorResponse.FileR\x04\x66ile\x1a\xb1\x01\n\x04\x46ile\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\'\n\x0finsertion_point\x18\x02 \x01(\tR\x0einsertionPoint\x12\x18\n\x07\x63ontent\x18\x0f \x01(\tR\x07\x63ontent\x12R\n\x13generated_code_info\x18\x10 \x01(\x0b\x32\".google.protobuf.GeneratedCodeInfoR\x11generatedCodeInfo\"8\n\x07\x46\x65\x61ture\x12\x10\n\x0c\x46\x45\x41TURE_NONE\x10\x00\x12\x1b\n\x17\x46\x45\x41TURE_PROTO3_OPTIONAL\x10\x01\x42r\n\x1c\x63om.google.protobuf.compilerB\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb\xaa\x02\x18Google.Protobuf.Compiler') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\034com.google.protobuf.compilerB\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb\252\002\030Google.Protobuf.Compiler' - _globals['_VERSION']._serialized_start=101 - _globals['_VERSION']._serialized_end=200 - _globals['_CODEGENERATORREQUEST']._serialized_start=203 - _globals['_CODEGENERATORREQUEST']._serialized_end=444 - _globals['_CODEGENERATORRESPONSE']._serialized_start=447 - _globals['_CODEGENERATORRESPONSE']._serialized_end=851 - _globals['_CODEGENERATORRESPONSE_FILE']._serialized_start=616 - _globals['_CODEGENERATORRESPONSE_FILE']._serialized_end=793 - _globals['_CODEGENERATORRESPONSE_FEATURE']._serialized_start=795 - _globals['_CODEGENERATORRESPONSE_FEATURE']._serialized_end=851 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/descriptor.py b/script.module.protobuf/resources/lib/google/protobuf/descriptor.py deleted file mode 100644 index fcb87cab5..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/descriptor.py +++ /dev/null @@ -1,1266 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Descriptors essentially contain exactly the information found in a .proto -file, in types that make this information accessible in Python. -""" - -__author__ = 'robinson@google.com (Will Robinson)' - -import threading -import warnings - -from google.protobuf.internal import api_implementation - -_USE_C_DESCRIPTORS = False -if api_implementation.Type() != 'python': - # Used by MakeDescriptor in cpp mode - import binascii - import os - # pylint: disable=protected-access - _message = api_implementation._c_module - # TODO(jieluo): Remove this import after fix api_implementation - if _message is None: - from google.protobuf.pyext import _message - _USE_C_DESCRIPTORS = True - - -class Error(Exception): - """Base error for this module.""" - - -class TypeTransformationError(Error): - """Error transforming between python proto type and corresponding C++ type.""" - - -if _USE_C_DESCRIPTORS: - # This metaclass allows to override the behavior of code like - # isinstance(my_descriptor, FieldDescriptor) - # and make it return True when the descriptor is an instance of the extension - # type written in C++. - class DescriptorMetaclass(type): - - def __instancecheck__(cls, obj): - if super(DescriptorMetaclass, cls).__instancecheck__(obj): - return True - if isinstance(obj, cls._C_DESCRIPTOR_CLASS): - return True - return False -else: - # The standard metaclass; nothing changes. - DescriptorMetaclass = type - - -class _Lock(object): - """Wrapper class of threading.Lock(), which is allowed by 'with'.""" - - def __new__(cls): - self = object.__new__(cls) - self._lock = threading.Lock() # pylint: disable=protected-access - return self - - def __enter__(self): - self._lock.acquire() - - def __exit__(self, exc_type, exc_value, exc_tb): - self._lock.release() - - -_lock = threading.Lock() - - -def _Deprecated(name): - if _Deprecated.count > 0: - _Deprecated.count -= 1 - warnings.warn( - 'Call to deprecated create function %s(). Note: Create unlinked ' - 'descriptors is going to go away. Please use get/find descriptors from ' - 'generated code or query the descriptor_pool.' - % name, - category=DeprecationWarning, stacklevel=3) - - -# Deprecated warnings will print 100 times at most which should be enough for -# users to notice and do not cause timeout. -_Deprecated.count = 100 - - -_internal_create_key = object() - - -class DescriptorBase(metaclass=DescriptorMetaclass): - - """Descriptors base class. - - This class is the base of all descriptor classes. It provides common options - related functionality. - - Attributes: - has_options: True if the descriptor has non-default options. Usually it - is not necessary to read this -- just call GetOptions() which will - happily return the default instance. However, it's sometimes useful - for efficiency, and also useful inside the protobuf implementation to - avoid some bootstrapping issues. - """ - - if _USE_C_DESCRIPTORS: - # The class, or tuple of classes, that are considered as "virtual - # subclasses" of this descriptor class. - _C_DESCRIPTOR_CLASS = () - - def __init__(self, options, serialized_options, options_class_name): - """Initialize the descriptor given its options message and the name of the - class of the options message. The name of the class is required in case - the options message is None and has to be created. - """ - self._options = options - self._options_class_name = options_class_name - self._serialized_options = serialized_options - - # Does this descriptor have non-default options? - self.has_options = (options is not None) or (serialized_options is not None) - - def _SetOptions(self, options, options_class_name): - """Sets the descriptor's options - - This function is used in generated proto2 files to update descriptor - options. It must not be used outside proto2. - """ - self._options = options - self._options_class_name = options_class_name - - # Does this descriptor have non-default options? - self.has_options = options is not None - - def GetOptions(self): - """Retrieves descriptor options. - - This method returns the options set or creates the default options for the - descriptor. - """ - if self._options: - return self._options - - from google.protobuf import descriptor_pb2 - try: - options_class = getattr(descriptor_pb2, - self._options_class_name) - except AttributeError: - raise RuntimeError('Unknown options class name %s!' % - (self._options_class_name)) - - with _lock: - if self._serialized_options is None: - self._options = options_class() - else: - self._options = _ParseOptions(options_class(), - self._serialized_options) - - return self._options - - -class _NestedDescriptorBase(DescriptorBase): - """Common class for descriptors that can be nested.""" - - def __init__(self, options, options_class_name, name, full_name, - file, containing_type, serialized_start=None, - serialized_end=None, serialized_options=None): - """Constructor. - - Args: - options: Protocol message options or None - to use default message options. - options_class_name (str): The class name of the above options. - name (str): Name of this protocol message type. - full_name (str): Fully-qualified name of this protocol message type, - which will include protocol "package" name and the name of any - enclosing types. - file (FileDescriptor): Reference to file info. - containing_type: if provided, this is a nested descriptor, with this - descriptor as parent, otherwise None. - serialized_start: The start index (inclusive) in block in the - file.serialized_pb that describes this descriptor. - serialized_end: The end index (exclusive) in block in the - file.serialized_pb that describes this descriptor. - serialized_options: Protocol message serialized options or None. - """ - super(_NestedDescriptorBase, self).__init__( - options, serialized_options, options_class_name) - - self.name = name - # TODO(falk): Add function to calculate full_name instead of having it in - # memory? - self.full_name = full_name - self.file = file - self.containing_type = containing_type - - self._serialized_start = serialized_start - self._serialized_end = serialized_end - - def CopyToProto(self, proto): - """Copies this to the matching proto in descriptor_pb2. - - Args: - proto: An empty proto instance from descriptor_pb2. - - Raises: - Error: If self couldn't be serialized, due to to few constructor - arguments. - """ - if (self.file is not None and - self._serialized_start is not None and - self._serialized_end is not None): - proto.ParseFromString(self.file.serialized_pb[ - self._serialized_start:self._serialized_end]) - else: - raise Error('Descriptor does not contain serialization.') - - -class Descriptor(_NestedDescriptorBase): - - """Descriptor for a protocol message type. - - Attributes: - name (str): Name of this protocol message type. - full_name (str): Fully-qualified name of this protocol message type, - which will include protocol "package" name and the name of any - enclosing types. - containing_type (Descriptor): Reference to the descriptor of the type - containing us, or None if this is top-level. - fields (list[FieldDescriptor]): Field descriptors for all fields in - this type. - fields_by_number (dict(int, FieldDescriptor)): Same - :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed - by "number" attribute in each FieldDescriptor. - fields_by_name (dict(str, FieldDescriptor)): Same - :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by - "name" attribute in each :class:`FieldDescriptor`. - nested_types (list[Descriptor]): Descriptor references - for all protocol message types nested within this one. - nested_types_by_name (dict(str, Descriptor)): Same Descriptor - objects as in :attr:`nested_types`, but indexed by "name" attribute - in each Descriptor. - enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references - for all enums contained within this type. - enum_types_by_name (dict(str, EnumDescriptor)): Same - :class:`EnumDescriptor` objects as in :attr:`enum_types`, but - indexed by "name" attribute in each EnumDescriptor. - enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping - from enum value name to :class:`EnumValueDescriptor` for that value. - extensions (list[FieldDescriptor]): All extensions defined directly - within this message type (NOT within a nested type). - extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor - objects as :attr:`extensions`, but indexed by "name" attribute of each - FieldDescriptor. - is_extendable (bool): Does this type define any extension ranges? - oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields - in this message. - oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in - :attr:`oneofs`, but indexed by "name" attribute. - file (FileDescriptor): Reference to file descriptor. - - """ - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.Descriptor - - def __new__( - cls, - name=None, - full_name=None, - filename=None, - containing_type=None, - fields=None, - nested_types=None, - enum_types=None, - extensions=None, - options=None, - serialized_options=None, - is_extendable=True, - extension_ranges=None, - oneofs=None, - file=None, # pylint: disable=redefined-builtin - serialized_start=None, - serialized_end=None, - syntax=None, - create_key=None): - _message.Message._CheckCalledFromGeneratedFile() - return _message.default_pool.FindMessageTypeByName(full_name) - - # NOTE(tmarek): The file argument redefining a builtin is nothing we can - # fix right now since we don't know how many clients already rely on the - # name of the argument. - def __init__(self, name, full_name, filename, containing_type, fields, - nested_types, enum_types, extensions, options=None, - serialized_options=None, - is_extendable=True, extension_ranges=None, oneofs=None, - file=None, serialized_start=None, serialized_end=None, # pylint: disable=redefined-builtin - syntax=None, create_key=None): - """Arguments to __init__() are as described in the description - of Descriptor fields above. - - Note that filename is an obsolete argument, that is not used anymore. - Please use file.name to access this as an attribute. - """ - if create_key is not _internal_create_key: - _Deprecated('Descriptor') - - super(Descriptor, self).__init__( - options, 'MessageOptions', name, full_name, file, - containing_type, serialized_start=serialized_start, - serialized_end=serialized_end, serialized_options=serialized_options) - - # We have fields in addition to fields_by_name and fields_by_number, - # so that: - # 1. Clients can index fields by "order in which they're listed." - # 2. Clients can easily iterate over all fields with the terse - # syntax: for f in descriptor.fields: ... - self.fields = fields - for field in self.fields: - field.containing_type = self - self.fields_by_number = dict((f.number, f) for f in fields) - self.fields_by_name = dict((f.name, f) for f in fields) - self._fields_by_camelcase_name = None - - self.nested_types = nested_types - for nested_type in nested_types: - nested_type.containing_type = self - self.nested_types_by_name = dict((t.name, t) for t in nested_types) - - self.enum_types = enum_types - for enum_type in self.enum_types: - enum_type.containing_type = self - self.enum_types_by_name = dict((t.name, t) for t in enum_types) - self.enum_values_by_name = dict( - (v.name, v) for t in enum_types for v in t.values) - - self.extensions = extensions - for extension in self.extensions: - extension.extension_scope = self - self.extensions_by_name = dict((f.name, f) for f in extensions) - self.is_extendable = is_extendable - self.extension_ranges = extension_ranges - self.oneofs = oneofs if oneofs is not None else [] - self.oneofs_by_name = dict((o.name, o) for o in self.oneofs) - for oneof in self.oneofs: - oneof.containing_type = self - self.syntax = syntax or "proto2" - - @property - def fields_by_camelcase_name(self): - """Same FieldDescriptor objects as in :attr:`fields`, but indexed by - :attr:`FieldDescriptor.camelcase_name`. - """ - if self._fields_by_camelcase_name is None: - self._fields_by_camelcase_name = dict( - (f.camelcase_name, f) for f in self.fields) - return self._fields_by_camelcase_name - - def EnumValueName(self, enum, value): - """Returns the string name of an enum value. - - This is just a small helper method to simplify a common operation. - - Args: - enum: string name of the Enum. - value: int, value of the enum. - - Returns: - string name of the enum value. - - Raises: - KeyError if either the Enum doesn't exist or the value is not a valid - value for the enum. - """ - return self.enum_types_by_name[enum].values_by_number[value].name - - def CopyToProto(self, proto): - """Copies this to a descriptor_pb2.DescriptorProto. - - Args: - proto: An empty descriptor_pb2.DescriptorProto. - """ - # This function is overridden to give a better doc comment. - super(Descriptor, self).CopyToProto(proto) - - -# TODO(robinson): We should have aggressive checking here, -# for example: -# * If you specify a repeated field, you should not be allowed -# to specify a default value. -# * [Other examples here as needed]. -# -# TODO(robinson): for this and other *Descriptor classes, we -# might also want to lock things down aggressively (e.g., -# prevent clients from setting the attributes). Having -# stronger invariants here in general will reduce the number -# of runtime checks we must do in reflection.py... -class FieldDescriptor(DescriptorBase): - - """Descriptor for a single field in a .proto file. - - Attributes: - name (str): Name of this field, exactly as it appears in .proto. - full_name (str): Name of this field, including containing scope. This is - particularly relevant for extensions. - index (int): Dense, 0-indexed index giving the order that this - field textually appears within its message in the .proto file. - number (int): Tag number declared for this field in the .proto file. - - type (int): (One of the TYPE_* constants below) Declared type. - cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to - represent this field. - - label (int): (One of the LABEL_* constants below) Tells whether this - field is optional, required, or repeated. - has_default_value (bool): True if this field has a default value defined, - otherwise false. - default_value (Varies): Default value of this field. Only - meaningful for non-repeated scalar fields. Repeated fields - should always set this to [], and non-repeated composite - fields should always set this to None. - - containing_type (Descriptor): Descriptor of the protocol message - type that contains this field. Set by the Descriptor constructor - if we're passed into one. - Somewhat confusingly, for extension fields, this is the - descriptor of the EXTENDED message, not the descriptor - of the message containing this field. (See is_extension and - extension_scope below). - message_type (Descriptor): If a composite field, a descriptor - of the message type contained in this field. Otherwise, this is None. - enum_type (EnumDescriptor): If this field contains an enum, a - descriptor of that enum. Otherwise, this is None. - - is_extension: True iff this describes an extension field. - extension_scope (Descriptor): Only meaningful if is_extension is True. - Gives the message that immediately contains this extension field. - Will be None iff we're a top-level (file-level) extension field. - - options (descriptor_pb2.FieldOptions): Protocol message field options or - None to use default field options. - - containing_oneof (OneofDescriptor): If the field is a member of a oneof - union, contains its descriptor. Otherwise, None. - - file (FileDescriptor): Reference to file descriptor. - """ - - # Must be consistent with C++ FieldDescriptor::Type enum in - # descriptor.h. - # - # TODO(robinson): Find a way to eliminate this repetition. - TYPE_DOUBLE = 1 - TYPE_FLOAT = 2 - TYPE_INT64 = 3 - TYPE_UINT64 = 4 - TYPE_INT32 = 5 - TYPE_FIXED64 = 6 - TYPE_FIXED32 = 7 - TYPE_BOOL = 8 - TYPE_STRING = 9 - TYPE_GROUP = 10 - TYPE_MESSAGE = 11 - TYPE_BYTES = 12 - TYPE_UINT32 = 13 - TYPE_ENUM = 14 - TYPE_SFIXED32 = 15 - TYPE_SFIXED64 = 16 - TYPE_SINT32 = 17 - TYPE_SINT64 = 18 - MAX_TYPE = 18 - - # Must be consistent with C++ FieldDescriptor::CppType enum in - # descriptor.h. - # - # TODO(robinson): Find a way to eliminate this repetition. - CPPTYPE_INT32 = 1 - CPPTYPE_INT64 = 2 - CPPTYPE_UINT32 = 3 - CPPTYPE_UINT64 = 4 - CPPTYPE_DOUBLE = 5 - CPPTYPE_FLOAT = 6 - CPPTYPE_BOOL = 7 - CPPTYPE_ENUM = 8 - CPPTYPE_STRING = 9 - CPPTYPE_MESSAGE = 10 - MAX_CPPTYPE = 10 - - _PYTHON_TO_CPP_PROTO_TYPE_MAP = { - TYPE_DOUBLE: CPPTYPE_DOUBLE, - TYPE_FLOAT: CPPTYPE_FLOAT, - TYPE_ENUM: CPPTYPE_ENUM, - TYPE_INT64: CPPTYPE_INT64, - TYPE_SINT64: CPPTYPE_INT64, - TYPE_SFIXED64: CPPTYPE_INT64, - TYPE_UINT64: CPPTYPE_UINT64, - TYPE_FIXED64: CPPTYPE_UINT64, - TYPE_INT32: CPPTYPE_INT32, - TYPE_SFIXED32: CPPTYPE_INT32, - TYPE_SINT32: CPPTYPE_INT32, - TYPE_UINT32: CPPTYPE_UINT32, - TYPE_FIXED32: CPPTYPE_UINT32, - TYPE_BYTES: CPPTYPE_STRING, - TYPE_STRING: CPPTYPE_STRING, - TYPE_BOOL: CPPTYPE_BOOL, - TYPE_MESSAGE: CPPTYPE_MESSAGE, - TYPE_GROUP: CPPTYPE_MESSAGE - } - - # Must be consistent with C++ FieldDescriptor::Label enum in - # descriptor.h. - # - # TODO(robinson): Find a way to eliminate this repetition. - LABEL_OPTIONAL = 1 - LABEL_REQUIRED = 2 - LABEL_REPEATED = 3 - MAX_LABEL = 3 - - # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber, - # and kLastReservedNumber in descriptor.h - MAX_FIELD_NUMBER = (1 << 29) - 1 - FIRST_RESERVED_FIELD_NUMBER = 19000 - LAST_RESERVED_FIELD_NUMBER = 19999 - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.FieldDescriptor - - def __new__(cls, name, full_name, index, number, type, cpp_type, label, - default_value, message_type, enum_type, containing_type, - is_extension, extension_scope, options=None, - serialized_options=None, - has_default_value=True, containing_oneof=None, json_name=None, - file=None, create_key=None): # pylint: disable=redefined-builtin - _message.Message._CheckCalledFromGeneratedFile() - if is_extension: - return _message.default_pool.FindExtensionByName(full_name) - else: - return _message.default_pool.FindFieldByName(full_name) - - def __init__(self, name, full_name, index, number, type, cpp_type, label, - default_value, message_type, enum_type, containing_type, - is_extension, extension_scope, options=None, - serialized_options=None, - has_default_value=True, containing_oneof=None, json_name=None, - file=None, create_key=None): # pylint: disable=redefined-builtin - """The arguments are as described in the description of FieldDescriptor - attributes above. - - Note that containing_type may be None, and may be set later if necessary - (to deal with circular references between message types, for example). - Likewise for extension_scope. - """ - if create_key is not _internal_create_key: - _Deprecated('FieldDescriptor') - - super(FieldDescriptor, self).__init__( - options, serialized_options, 'FieldOptions') - self.name = name - self.full_name = full_name - self.file = file - self._camelcase_name = None - if json_name is None: - self.json_name = _ToJsonName(name) - else: - self.json_name = json_name - self.index = index - self.number = number - self.type = type - self.cpp_type = cpp_type - self.label = label - self.has_default_value = has_default_value - self.default_value = default_value - self.containing_type = containing_type - self.message_type = message_type - self.enum_type = enum_type - self.is_extension = is_extension - self.extension_scope = extension_scope - self.containing_oneof = containing_oneof - if api_implementation.Type() == 'python': - self._cdescriptor = None - else: - if is_extension: - self._cdescriptor = _message.default_pool.FindExtensionByName(full_name) - else: - self._cdescriptor = _message.default_pool.FindFieldByName(full_name) - - @property - def camelcase_name(self): - """Camelcase name of this field. - - Returns: - str: the name in CamelCase. - """ - if self._camelcase_name is None: - self._camelcase_name = _ToCamelCase(self.name) - return self._camelcase_name - - @property - def has_presence(self): - """Whether the field distinguishes between unpopulated and default values. - - Raises: - RuntimeError: singular field that is not linked with message nor file. - """ - if self.label == FieldDescriptor.LABEL_REPEATED: - return False - if (self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE or - self.containing_oneof): - return True - # self.containing_type is used here instead of self.file for legacy - # compatibility. FieldDescriptor.file was added in cl/153110619 - # Some old/generated code didn't link file to FieldDescriptor. - # TODO(jieluo): remove syntax usage b/240619313 - return self.containing_type.syntax == 'proto2' - - @property - def is_packed(self): - """Returns if the field is packed.""" - if self.label != FieldDescriptor.LABEL_REPEATED: - return False - field_type = self.type - if (field_type == FieldDescriptor.TYPE_STRING or - field_type == FieldDescriptor.TYPE_GROUP or - field_type == FieldDescriptor.TYPE_MESSAGE or - field_type == FieldDescriptor.TYPE_BYTES): - return False - if self.containing_type.syntax == 'proto2': - return self.has_options and self.GetOptions().packed - else: - return (not self.has_options or - not self.GetOptions().HasField('packed') or - self.GetOptions().packed) - - @staticmethod - def ProtoTypeToCppProtoType(proto_type): - """Converts from a Python proto type to a C++ Proto Type. - - The Python ProtocolBuffer classes specify both the 'Python' datatype and the - 'C++' datatype - and they're not the same. This helper method should - translate from one to another. - - Args: - proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*) - Returns: - int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type. - Raises: - TypeTransformationError: when the Python proto type isn't known. - """ - try: - return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type] - except KeyError: - raise TypeTransformationError('Unknown proto_type: %s' % proto_type) - - -class EnumDescriptor(_NestedDescriptorBase): - - """Descriptor for an enum defined in a .proto file. - - Attributes: - name (str): Name of the enum type. - full_name (str): Full name of the type, including package name - and any enclosing type(s). - - values (list[EnumValueDescriptor]): List of the values - in this enum. - values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`, - but indexed by the "name" field of each EnumValueDescriptor. - values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`, - but indexed by the "number" field of each EnumValueDescriptor. - containing_type (Descriptor): Descriptor of the immediate containing - type of this enum, or None if this is an enum defined at the - top level in a .proto file. Set by Descriptor's constructor - if we're passed into one. - file (FileDescriptor): Reference to file descriptor. - options (descriptor_pb2.EnumOptions): Enum options message or - None to use default enum options. - """ - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.EnumDescriptor - - def __new__(cls, name, full_name, filename, values, - containing_type=None, options=None, - serialized_options=None, file=None, # pylint: disable=redefined-builtin - serialized_start=None, serialized_end=None, create_key=None): - _message.Message._CheckCalledFromGeneratedFile() - return _message.default_pool.FindEnumTypeByName(full_name) - - def __init__(self, name, full_name, filename, values, - containing_type=None, options=None, - serialized_options=None, file=None, # pylint: disable=redefined-builtin - serialized_start=None, serialized_end=None, create_key=None): - """Arguments are as described in the attribute description above. - - Note that filename is an obsolete argument, that is not used anymore. - Please use file.name to access this as an attribute. - """ - if create_key is not _internal_create_key: - _Deprecated('EnumDescriptor') - - super(EnumDescriptor, self).__init__( - options, 'EnumOptions', name, full_name, file, - containing_type, serialized_start=serialized_start, - serialized_end=serialized_end, serialized_options=serialized_options) - - self.values = values - for value in self.values: - value.type = self - self.values_by_name = dict((v.name, v) for v in values) - # Values are reversed to ensure that the first alias is retained. - self.values_by_number = dict((v.number, v) for v in reversed(values)) - - @property - def is_closed(self): - """Returns true whether this is a "closed" enum. - - This means that it: - - Has a fixed set of values, rather than being equivalent to an int32. - - Encountering values not in this set causes them to be treated as unknown - fields. - - The first value (i.e., the default) may be nonzero. - - WARNING: Some runtimes currently have a quirk where non-closed enums are - treated as closed when used as the type of fields defined in a - `syntax = proto2;` file. This quirk is not present in all runtimes; as of - writing, we know that: - - - C++, Java, and C++-based Python share this quirk. - - UPB and UPB-based Python do not. - - PHP and Ruby treat all enums as open regardless of declaration. - - Care should be taken when using this function to respect the target - runtime's enum handling quirks. - """ - return self.file.syntax == 'proto2' - - def CopyToProto(self, proto): - """Copies this to a descriptor_pb2.EnumDescriptorProto. - - Args: - proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto. - """ - # This function is overridden to give a better doc comment. - super(EnumDescriptor, self).CopyToProto(proto) - - -class EnumValueDescriptor(DescriptorBase): - - """Descriptor for a single value within an enum. - - Attributes: - name (str): Name of this value. - index (int): Dense, 0-indexed index giving the order that this - value appears textually within its enum in the .proto file. - number (int): Actual number assigned to this enum value. - type (EnumDescriptor): :class:`EnumDescriptor` to which this value - belongs. Set by :class:`EnumDescriptor`'s constructor if we're - passed into one. - options (descriptor_pb2.EnumValueOptions): Enum value options message or - None to use default enum value options options. - """ - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor - - def __new__(cls, name, index, number, - type=None, # pylint: disable=redefined-builtin - options=None, serialized_options=None, create_key=None): - _message.Message._CheckCalledFromGeneratedFile() - # There is no way we can build a complete EnumValueDescriptor with the - # given parameters (the name of the Enum is not known, for example). - # Fortunately generated files just pass it to the EnumDescriptor() - # constructor, which will ignore it, so returning None is good enough. - return None - - def __init__(self, name, index, number, - type=None, # pylint: disable=redefined-builtin - options=None, serialized_options=None, create_key=None): - """Arguments are as described in the attribute description above.""" - if create_key is not _internal_create_key: - _Deprecated('EnumValueDescriptor') - - super(EnumValueDescriptor, self).__init__( - options, serialized_options, 'EnumValueOptions') - self.name = name - self.index = index - self.number = number - self.type = type - - -class OneofDescriptor(DescriptorBase): - """Descriptor for a oneof field. - - Attributes: - name (str): Name of the oneof field. - full_name (str): Full name of the oneof field, including package name. - index (int): 0-based index giving the order of the oneof field inside - its containing type. - containing_type (Descriptor): :class:`Descriptor` of the protocol message - type that contains this field. Set by the :class:`Descriptor` constructor - if we're passed into one. - fields (list[FieldDescriptor]): The list of field descriptors this - oneof can contain. - """ - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.OneofDescriptor - - def __new__( - cls, name, full_name, index, containing_type, fields, options=None, - serialized_options=None, create_key=None): - _message.Message._CheckCalledFromGeneratedFile() - return _message.default_pool.FindOneofByName(full_name) - - def __init__( - self, name, full_name, index, containing_type, fields, options=None, - serialized_options=None, create_key=None): - """Arguments are as described in the attribute description above.""" - if create_key is not _internal_create_key: - _Deprecated('OneofDescriptor') - - super(OneofDescriptor, self).__init__( - options, serialized_options, 'OneofOptions') - self.name = name - self.full_name = full_name - self.index = index - self.containing_type = containing_type - self.fields = fields - - -class ServiceDescriptor(_NestedDescriptorBase): - - """Descriptor for a service. - - Attributes: - name (str): Name of the service. - full_name (str): Full name of the service, including package name. - index (int): 0-indexed index giving the order that this services - definition appears within the .proto file. - methods (list[MethodDescriptor]): List of methods provided by this - service. - methods_by_name (dict(str, MethodDescriptor)): Same - :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but - indexed by "name" attribute in each :class:`MethodDescriptor`. - options (descriptor_pb2.ServiceOptions): Service options message or - None to use default service options. - file (FileDescriptor): Reference to file info. - """ - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor - - def __new__( - cls, - name=None, - full_name=None, - index=None, - methods=None, - options=None, - serialized_options=None, - file=None, # pylint: disable=redefined-builtin - serialized_start=None, - serialized_end=None, - create_key=None): - _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access - return _message.default_pool.FindServiceByName(full_name) - - def __init__(self, name, full_name, index, methods, options=None, - serialized_options=None, file=None, # pylint: disable=redefined-builtin - serialized_start=None, serialized_end=None, create_key=None): - if create_key is not _internal_create_key: - _Deprecated('ServiceDescriptor') - - super(ServiceDescriptor, self).__init__( - options, 'ServiceOptions', name, full_name, file, - None, serialized_start=serialized_start, - serialized_end=serialized_end, serialized_options=serialized_options) - self.index = index - self.methods = methods - self.methods_by_name = dict((m.name, m) for m in methods) - # Set the containing service for each method in this service. - for method in self.methods: - method.containing_service = self - - def FindMethodByName(self, name): - """Searches for the specified method, and returns its descriptor. - - Args: - name (str): Name of the method. - - Returns: - MethodDescriptor: The descriptor for the requested method. - - Raises: - KeyError: if the method cannot be found in the service. - """ - return self.methods_by_name[name] - - def CopyToProto(self, proto): - """Copies this to a descriptor_pb2.ServiceDescriptorProto. - - Args: - proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto. - """ - # This function is overridden to give a better doc comment. - super(ServiceDescriptor, self).CopyToProto(proto) - - -class MethodDescriptor(DescriptorBase): - - """Descriptor for a method in a service. - - Attributes: - name (str): Name of the method within the service. - full_name (str): Full name of method. - index (int): 0-indexed index of the method inside the service. - containing_service (ServiceDescriptor): The service that contains this - method. - input_type (Descriptor): The descriptor of the message that this method - accepts. - output_type (Descriptor): The descriptor of the message that this method - returns. - client_streaming (bool): Whether this method uses client streaming. - server_streaming (bool): Whether this method uses server streaming. - options (descriptor_pb2.MethodOptions or None): Method options message, or - None to use default method options. - """ - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.MethodDescriptor - - def __new__(cls, - name, - full_name, - index, - containing_service, - input_type, - output_type, - client_streaming=False, - server_streaming=False, - options=None, - serialized_options=None, - create_key=None): - _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access - return _message.default_pool.FindMethodByName(full_name) - - def __init__(self, - name, - full_name, - index, - containing_service, - input_type, - output_type, - client_streaming=False, - server_streaming=False, - options=None, - serialized_options=None, - create_key=None): - """The arguments are as described in the description of MethodDescriptor - attributes above. - - Note that containing_service may be None, and may be set later if necessary. - """ - if create_key is not _internal_create_key: - _Deprecated('MethodDescriptor') - - super(MethodDescriptor, self).__init__( - options, serialized_options, 'MethodOptions') - self.name = name - self.full_name = full_name - self.index = index - self.containing_service = containing_service - self.input_type = input_type - self.output_type = output_type - self.client_streaming = client_streaming - self.server_streaming = server_streaming - - def CopyToProto(self, proto): - """Copies this to a descriptor_pb2.MethodDescriptorProto. - - Args: - proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto. - - Raises: - Error: If self couldn't be serialized, due to too few constructor - arguments. - """ - if self.containing_service is not None: - from google.protobuf import descriptor_pb2 - service_proto = descriptor_pb2.ServiceDescriptorProto() - self.containing_service.CopyToProto(service_proto) - proto.CopyFrom(service_proto.method[self.index]) - else: - raise Error('Descriptor does not contain a service.') - - -class FileDescriptor(DescriptorBase): - """Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto. - - Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and - :attr:`dependencies` fields are only set by the - :py:mod:`google.protobuf.message_factory` module, and not by the generated - proto code. - - Attributes: - name (str): Name of file, relative to root of source tree. - package (str): Name of the package - syntax (str): string indicating syntax of the file (can be "proto2" or - "proto3") - serialized_pb (bytes): Byte string of serialized - :class:`descriptor_pb2.FileDescriptorProto`. - dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor` - objects this :class:`FileDescriptor` depends on. - public_dependencies (list[FileDescriptor]): A subset of - :attr:`dependencies`, which were declared as "public". - message_types_by_name (dict(str, Descriptor)): Mapping from message names - to their :class:`Descriptor`. - enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to - their :class:`EnumDescriptor`. - extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension - names declared at file scope to their :class:`FieldDescriptor`. - services_by_name (dict(str, ServiceDescriptor)): Mapping from services' - names to their :class:`ServiceDescriptor`. - pool (DescriptorPool): The pool this descriptor belongs to. When not - passed to the constructor, the global default pool is used. - """ - - if _USE_C_DESCRIPTORS: - _C_DESCRIPTOR_CLASS = _message.FileDescriptor - - def __new__(cls, name, package, options=None, - serialized_options=None, serialized_pb=None, - dependencies=None, public_dependencies=None, - syntax=None, pool=None, create_key=None): - # FileDescriptor() is called from various places, not only from generated - # files, to register dynamic proto files and messages. - # pylint: disable=g-explicit-bool-comparison - if serialized_pb: - return _message.default_pool.AddSerializedFile(serialized_pb) - else: - return super(FileDescriptor, cls).__new__(cls) - - def __init__(self, name, package, options=None, - serialized_options=None, serialized_pb=None, - dependencies=None, public_dependencies=None, - syntax=None, pool=None, create_key=None): - """Constructor.""" - if create_key is not _internal_create_key: - _Deprecated('FileDescriptor') - - super(FileDescriptor, self).__init__( - options, serialized_options, 'FileOptions') - - if pool is None: - from google.protobuf import descriptor_pool - pool = descriptor_pool.Default() - self.pool = pool - self.message_types_by_name = {} - self.name = name - self.package = package - self.syntax = syntax or "proto2" - self.serialized_pb = serialized_pb - - self.enum_types_by_name = {} - self.extensions_by_name = {} - self.services_by_name = {} - self.dependencies = (dependencies or []) - self.public_dependencies = (public_dependencies or []) - - def CopyToProto(self, proto): - """Copies this to a descriptor_pb2.FileDescriptorProto. - - Args: - proto: An empty descriptor_pb2.FileDescriptorProto. - """ - proto.ParseFromString(self.serialized_pb) - - -def _ParseOptions(message, string): - """Parses serialized options. - - This helper function is used to parse serialized options in generated - proto2 files. It must not be used outside proto2. - """ - message.ParseFromString(string) - return message - - -def _ToCamelCase(name): - """Converts name to camel-case and returns it.""" - capitalize_next = False - result = [] - - for c in name: - if c == '_': - if result: - capitalize_next = True - elif capitalize_next: - result.append(c.upper()) - capitalize_next = False - else: - result += c - - # Lower-case the first letter. - if result and result[0].isupper(): - result[0] = result[0].lower() - return ''.join(result) - - -def _OptionsOrNone(descriptor_proto): - """Returns the value of the field `options`, or None if it is not set.""" - if descriptor_proto.HasField('options'): - return descriptor_proto.options - else: - return None - - -def _ToJsonName(name): - """Converts name to Json name and returns it.""" - capitalize_next = False - result = [] - - for c in name: - if c == '_': - capitalize_next = True - elif capitalize_next: - result.append(c.upper()) - capitalize_next = False - else: - result += c - - return ''.join(result) - - -def MakeDescriptor(desc_proto, package='', build_file_if_cpp=True, - syntax=None): - """Make a protobuf Descriptor given a DescriptorProto protobuf. - - Handles nested descriptors. Note that this is limited to the scope of defining - a message inside of another message. Composite fields can currently only be - resolved if the message is defined in the same scope as the field. - - Args: - desc_proto: The descriptor_pb2.DescriptorProto protobuf message. - package: Optional package name for the new message Descriptor (string). - build_file_if_cpp: Update the C++ descriptor pool if api matches. - Set to False on recursion, so no duplicates are created. - syntax: The syntax/semantics that should be used. Set to "proto3" to get - proto3 field presence semantics. - Returns: - A Descriptor for protobuf messages. - """ - if api_implementation.Type() != 'python' and build_file_if_cpp: - # The C++ implementation requires all descriptors to be backed by the same - # definition in the C++ descriptor pool. To do this, we build a - # FileDescriptorProto with the same definition as this descriptor and build - # it into the pool. - from google.protobuf import descriptor_pb2 - file_descriptor_proto = descriptor_pb2.FileDescriptorProto() - file_descriptor_proto.message_type.add().MergeFrom(desc_proto) - - # Generate a random name for this proto file to prevent conflicts with any - # imported ones. We need to specify a file name so the descriptor pool - # accepts our FileDescriptorProto, but it is not important what that file - # name is actually set to. - proto_name = binascii.hexlify(os.urandom(16)).decode('ascii') - - if package: - file_descriptor_proto.name = os.path.join(package.replace('.', '/'), - proto_name + '.proto') - file_descriptor_proto.package = package - else: - file_descriptor_proto.name = proto_name + '.proto' - - _message.default_pool.Add(file_descriptor_proto) - result = _message.default_pool.FindFileByName(file_descriptor_proto.name) - - if _USE_C_DESCRIPTORS: - return result.message_types_by_name[desc_proto.name] - - full_message_name = [desc_proto.name] - if package: full_message_name.insert(0, package) - - # Create Descriptors for enum types - enum_types = {} - for enum_proto in desc_proto.enum_type: - full_name = '.'.join(full_message_name + [enum_proto.name]) - enum_desc = EnumDescriptor( - enum_proto.name, full_name, None, [ - EnumValueDescriptor(enum_val.name, ii, enum_val.number, - create_key=_internal_create_key) - for ii, enum_val in enumerate(enum_proto.value)], - create_key=_internal_create_key) - enum_types[full_name] = enum_desc - - # Create Descriptors for nested types - nested_types = {} - for nested_proto in desc_proto.nested_type: - full_name = '.'.join(full_message_name + [nested_proto.name]) - # Nested types are just those defined inside of the message, not all types - # used by fields in the message, so no loops are possible here. - nested_desc = MakeDescriptor(nested_proto, - package='.'.join(full_message_name), - build_file_if_cpp=False, - syntax=syntax) - nested_types[full_name] = nested_desc - - fields = [] - for field_proto in desc_proto.field: - full_name = '.'.join(full_message_name + [field_proto.name]) - enum_desc = None - nested_desc = None - if field_proto.json_name: - json_name = field_proto.json_name - else: - json_name = None - if field_proto.HasField('type_name'): - type_name = field_proto.type_name - full_type_name = '.'.join(full_message_name + - [type_name[type_name.rfind('.')+1:]]) - if full_type_name in nested_types: - nested_desc = nested_types[full_type_name] - elif full_type_name in enum_types: - enum_desc = enum_types[full_type_name] - # Else type_name references a non-local type, which isn't implemented - field = FieldDescriptor( - field_proto.name, full_name, field_proto.number - 1, - field_proto.number, field_proto.type, - FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type), - field_proto.label, None, nested_desc, enum_desc, None, False, None, - options=_OptionsOrNone(field_proto), has_default_value=False, - json_name=json_name, create_key=_internal_create_key) - fields.append(field) - - desc_name = '.'.join(full_message_name) - return Descriptor(desc_proto.name, desc_name, None, None, fields, - list(nested_types.values()), list(enum_types.values()), [], - options=_OptionsOrNone(desc_proto), - create_key=_internal_create_key) diff --git a/script.module.protobuf/resources/lib/google/protobuf/descriptor_database.py b/script.module.protobuf/resources/lib/google/protobuf/descriptor_database.py deleted file mode 100644 index 073eddc71..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/descriptor_database.py +++ /dev/null @@ -1,177 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Provides a container for DescriptorProtos.""" - -__author__ = 'matthewtoia@google.com (Matt Toia)' - -import warnings - - -class Error(Exception): - pass - - -class DescriptorDatabaseConflictingDefinitionError(Error): - """Raised when a proto is added with the same name & different descriptor.""" - - -class DescriptorDatabase(object): - """A container accepting FileDescriptorProtos and maps DescriptorProtos.""" - - def __init__(self): - self._file_desc_protos_by_file = {} - self._file_desc_protos_by_symbol = {} - - def Add(self, file_desc_proto): - """Adds the FileDescriptorProto and its types to this database. - - Args: - file_desc_proto: The FileDescriptorProto to add. - Raises: - DescriptorDatabaseConflictingDefinitionError: if an attempt is made to - add a proto with the same name but different definition than an - existing proto in the database. - """ - proto_name = file_desc_proto.name - if proto_name not in self._file_desc_protos_by_file: - self._file_desc_protos_by_file[proto_name] = file_desc_proto - elif self._file_desc_protos_by_file[proto_name] != file_desc_proto: - raise DescriptorDatabaseConflictingDefinitionError( - '%s already added, but with different descriptor.' % proto_name) - else: - return - - # Add all the top-level descriptors to the index. - package = file_desc_proto.package - for message in file_desc_proto.message_type: - for name in _ExtractSymbols(message, package): - self._AddSymbol(name, file_desc_proto) - for enum in file_desc_proto.enum_type: - self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto) - for enum_value in enum.value: - self._file_desc_protos_by_symbol[ - '.'.join((package, enum_value.name))] = file_desc_proto - for extension in file_desc_proto.extension: - self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto) - for service in file_desc_proto.service: - self._AddSymbol(('.'.join((package, service.name))), file_desc_proto) - - def FindFileByName(self, name): - """Finds the file descriptor proto by file name. - - Typically the file name is a relative path ending to a .proto file. The - proto with the given name will have to have been added to this database - using the Add method or else an error will be raised. - - Args: - name: The file name to find. - - Returns: - The file descriptor proto matching the name. - - Raises: - KeyError if no file by the given name was added. - """ - - return self._file_desc_protos_by_file[name] - - def FindFileContainingSymbol(self, symbol): - """Finds the file descriptor proto containing the specified symbol. - - The symbol should be a fully qualified name including the file descriptor's - package and any containing messages. Some examples: - - 'some.package.name.Message' - 'some.package.name.Message.NestedEnum' - 'some.package.name.Message.some_field' - - The file descriptor proto containing the specified symbol must be added to - this database using the Add method or else an error will be raised. - - Args: - symbol: The fully qualified symbol name. - - Returns: - The file descriptor proto containing the symbol. - - Raises: - KeyError if no file contains the specified symbol. - """ - try: - return self._file_desc_protos_by_symbol[symbol] - except KeyError: - # Fields, enum values, and nested extensions are not in - # _file_desc_protos_by_symbol. Try to find the top level - # descriptor. Non-existent nested symbol under a valid top level - # descriptor can also be found. The behavior is the same with - # protobuf C++. - top_level, _, _ = symbol.rpartition('.') - try: - return self._file_desc_protos_by_symbol[top_level] - except KeyError: - # Raise the original symbol as a KeyError for better diagnostics. - raise KeyError(symbol) - - def FindFileContainingExtension(self, extendee_name, extension_number): - # TODO(jieluo): implement this API. - return None - - def FindAllExtensionNumbers(self, extendee_name): - # TODO(jieluo): implement this API. - return [] - - def _AddSymbol(self, name, file_desc_proto): - if name in self._file_desc_protos_by_symbol: - warn_msg = ('Conflict register for file "' + file_desc_proto.name + - '": ' + name + - ' is already defined in file "' + - self._file_desc_protos_by_symbol[name].name + '"') - warnings.warn(warn_msg, RuntimeWarning) - self._file_desc_protos_by_symbol[name] = file_desc_proto - - -def _ExtractSymbols(desc_proto, package): - """Pulls out all the symbols from a descriptor proto. - - Args: - desc_proto: The proto to extract symbols from. - package: The package containing the descriptor type. - - Yields: - The fully qualified name found in the descriptor. - """ - message_name = package + '.' + desc_proto.name if package else desc_proto.name - yield message_name - for nested_type in desc_proto.nested_type: - for symbol in _ExtractSymbols(nested_type, message_name): - yield symbol - for enum_type in desc_proto.enum_type: - yield '.'.join((message_name, enum_type.name)) diff --git a/script.module.protobuf/resources/lib/google/protobuf/descriptor_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/descriptor_pb2.py deleted file mode 100644 index c42e7b6d1..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/descriptor_pb2.py +++ /dev/null @@ -1,2227 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/descriptor.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR = _descriptor.FileDescriptor( - name='google/protobuf/descriptor.proto', - package='google.protobuf', - syntax='proto2', - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"M\n\x11\x46ileDescriptorSet\x12\x38\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProtoR\x04\x66ile\"\xfe\x04\n\x13\x46ileDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n\x07package\x18\x02 \x01(\tR\x07package\x12\x1e\n\ndependency\x18\x03 \x03(\tR\ndependency\x12+\n\x11public_dependency\x18\n \x03(\x05R\x10publicDependency\x12\'\n\x0fweak_dependency\x18\x0b \x03(\x05R\x0eweakDependency\x12\x43\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\x0bmessageType\x12\x41\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12\x41\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProtoR\x07service\x12\x43\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x36\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptionsR\x07options\x12I\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfoR\x0esourceCodeInfo\x12\x16\n\x06syntax\x18\x0c \x01(\tR\x06syntax\x12\x18\n\x07\x65\x64ition\x18\r \x01(\tR\x07\x65\x64ition\"\xb9\x06\n\x0f\x44\x65scriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12;\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\x05\x66ield\x12\x43\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x41\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\nnestedType\x12\x41\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12X\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRangeR\x0e\x65xtensionRange\x12\x44\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProtoR\toneofDecl\x12\x39\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptionsR\x07options\x12U\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\n \x03(\tR\x0creservedName\x1az\n\x0e\x45xtensionRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\x12@\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptionsR\x07options\x1a\x37\n\rReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\xad\x04\n\x15\x45xtensionRangeOptions\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\x12Y\n\x0b\x64\x65\x63laration\x18\x02 \x03(\x0b\x32\x32.google.protobuf.ExtensionRangeOptions.DeclarationB\x03\x88\x01\x02R\x0b\x64\x65\x63laration\x12h\n\x0cverification\x18\x03 \x01(\x0e\x32\x38.google.protobuf.ExtensionRangeOptions.VerificationState:\nUNVERIFIEDR\x0cverification\x1a\xb3\x01\n\x0b\x44\x65\x63laration\x12\x16\n\x06number\x18\x01 \x01(\x05R\x06number\x12\x1b\n\tfull_name\x18\x02 \x01(\tR\x08\x66ullName\x12\x12\n\x04type\x18\x03 \x01(\tR\x04type\x12#\n\x0bis_repeated\x18\x04 \x01(\x08\x42\x02\x18\x01R\nisRepeated\x12\x1a\n\x08reserved\x18\x05 \x01(\x08R\x08reserved\x12\x1a\n\x08repeated\x18\x06 \x01(\x08R\x08repeated\"4\n\x11VerificationState\x12\x0f\n\x0b\x44\x45\x43LARATION\x10\x00\x12\x0e\n\nUNVERIFIED\x10\x01*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xc1\x06\n\x14\x46ieldDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x03 \x01(\x05R\x06number\x12\x41\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.LabelR\x05label\x12>\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.TypeR\x04type\x12\x1b\n\ttype_name\x18\x06 \x01(\tR\x08typeName\x12\x1a\n\x08\x65xtendee\x18\x02 \x01(\tR\x08\x65xtendee\x12#\n\rdefault_value\x18\x07 \x01(\tR\x0c\x64\x65\x66\x61ultValue\x12\x1f\n\x0boneof_index\x18\t \x01(\x05R\noneofIndex\x12\x1b\n\tjson_name\x18\n \x01(\tR\x08jsonName\x12\x37\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptionsR\x07options\x12\'\n\x0fproto3_optional\x18\x11 \x01(\x08R\x0eproto3Optional\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"c\n\x14OneofDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x37\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptionsR\x07options\"\xe3\x02\n\x13\x45numDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12?\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProtoR\x05value\x12\x36\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptionsR\x07options\x12]\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\x05 \x03(\tR\x0creservedName\x1a;\n\x11\x45numReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\x83\x01\n\x18\x45numValueDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x02 \x01(\x05R\x06number\x12;\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptionsR\x07options\"\xa7\x01\n\x16ServiceDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12>\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProtoR\x06method\x12\x39\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptionsR\x07options\"\x89\x02\n\x15MethodDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n\ninput_type\x18\x02 \x01(\tR\tinputType\x12\x1f\n\x0boutput_type\x18\x03 \x01(\tR\noutputType\x12\x38\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptionsR\x07options\x12\x30\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lseR\x0f\x63lientStreaming\x12\x30\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lseR\x0fserverStreaming\"\x91\t\n\x0b\x46ileOptions\x12!\n\x0cjava_package\x18\x01 \x01(\tR\x0bjavaPackage\x12\x30\n\x14java_outer_classname\x18\x08 \x01(\tR\x12javaOuterClassname\x12\x35\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lseR\x11javaMultipleFiles\x12\x44\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01R\x19javaGenerateEqualsAndHash\x12:\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lseR\x13javaStringCheckUtf8\x12S\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEEDR\x0boptimizeFor\x12\x1d\n\ngo_package\x18\x0b \x01(\tR\tgoPackage\x12\x35\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lseR\x11\x63\x63GenericServices\x12\x39\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lseR\x13javaGenericServices\x12\x35\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lseR\x11pyGenericServices\x12\x37\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lseR\x12phpGenericServices\x12%\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12.\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04trueR\x0e\x63\x63\x45nableArenas\x12*\n\x11objc_class_prefix\x18$ \x01(\tR\x0fobjcClassPrefix\x12)\n\x10\x63sharp_namespace\x18% \x01(\tR\x0f\x63sharpNamespace\x12!\n\x0cswift_prefix\x18\' \x01(\tR\x0bswiftPrefix\x12(\n\x10php_class_prefix\x18( \x01(\tR\x0ephpClassPrefix\x12#\n\rphp_namespace\x18) \x01(\tR\x0cphpNamespace\x12\x34\n\x16php_metadata_namespace\x18, \x01(\tR\x14phpMetadataNamespace\x12!\n\x0cruby_package\x18- \x01(\tR\x0brubyPackage\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\xbb\x03\n\x0eMessageOptions\x12<\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lseR\x14messageSetWireFormat\x12L\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lseR\x1cnoStandardDescriptorAccessor\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x1b\n\tmap_entry\x18\x07 \x01(\x08R\x08mapEntry\x12V\n&deprecated_legacy_json_field_conflicts\x18\x0b \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\x85\t\n\x0c\x46ieldOptions\x12\x41\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRINGR\x05\x63type\x12\x16\n\x06packed\x18\x02 \x01(\x08R\x06packed\x12G\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMALR\x06jstype\x12\x19\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lseR\x04lazy\x12.\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lseR\x0eunverifiedLazy\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x19\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lseR\x04weak\x12(\n\x0c\x64\x65\x62ug_redact\x18\x10 \x01(\x08:\x05\x66\x61lseR\x0b\x64\x65\x62ugRedact\x12K\n\tretention\x18\x11 \x01(\x0e\x32-.google.protobuf.FieldOptions.OptionRetentionR\tretention\x12J\n\x06target\x18\x12 \x01(\x0e\x32..google.protobuf.FieldOptions.OptionTargetTypeB\x02\x18\x01R\x06target\x12H\n\x07targets\x18\x13 \x03(\x0e\x32..google.protobuf.FieldOptions.OptionTargetTypeR\x07targets\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02\"U\n\x0fOptionRetention\x12\x15\n\x11RETENTION_UNKNOWN\x10\x00\x12\x15\n\x11RETENTION_RUNTIME\x10\x01\x12\x14\n\x10RETENTION_SOURCE\x10\x02\"\x8c\x02\n\x10OptionTargetType\x12\x17\n\x13TARGET_TYPE_UNKNOWN\x10\x00\x12\x14\n\x10TARGET_TYPE_FILE\x10\x01\x12\x1f\n\x1bTARGET_TYPE_EXTENSION_RANGE\x10\x02\x12\x17\n\x13TARGET_TYPE_MESSAGE\x10\x03\x12\x15\n\x11TARGET_TYPE_FIELD\x10\x04\x12\x15\n\x11TARGET_TYPE_ONEOF\x10\x05\x12\x14\n\x10TARGET_TYPE_ENUM\x10\x06\x12\x1a\n\x16TARGET_TYPE_ENUM_ENTRY\x10\x07\x12\x17\n\x13TARGET_TYPE_SERVICE\x10\x08\x12\x16\n\x12TARGET_TYPE_METHOD\x10\t*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"s\n\x0cOneofOptions\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x98\x02\n\x0b\x45numOptions\x12\x1f\n\x0b\x61llow_alias\x18\x02 \x01(\x08R\nallowAlias\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12V\n&deprecated_legacy_json_field_conflicts\x18\x06 \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"\x9e\x01\n\x10\x45numValueOptions\x12%\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9c\x01\n\x0eServiceOptions\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xe0\x02\n\rMethodOptions\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12q\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWNR\x10idempotencyLevel\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9a\x03\n\x13UninterpretedOption\x12\x41\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePartR\x04name\x12)\n\x10identifier_value\x18\x03 \x01(\tR\x0fidentifierValue\x12,\n\x12positive_int_value\x18\x04 \x01(\x04R\x10positiveIntValue\x12,\n\x12negative_int_value\x18\x05 \x01(\x03R\x10negativeIntValue\x12!\n\x0c\x64ouble_value\x18\x06 \x01(\x01R\x0b\x64oubleValue\x12!\n\x0cstring_value\x18\x07 \x01(\x0cR\x0bstringValue\x12\'\n\x0f\x61ggregate_value\x18\x08 \x01(\tR\x0e\x61ggregateValue\x1aJ\n\x08NamePart\x12\x1b\n\tname_part\x18\x01 \x02(\tR\x08namePart\x12!\n\x0cis_extension\x18\x02 \x02(\x08R\x0bisExtension\"\xa7\x02\n\x0eSourceCodeInfo\x12\x44\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.LocationR\x08location\x1a\xce\x01\n\x08Location\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x16\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01R\x04span\x12)\n\x10leading_comments\x18\x03 \x01(\tR\x0fleadingComments\x12+\n\x11trailing_comments\x18\x04 \x01(\tR\x10trailingComments\x12:\n\x19leading_detached_comments\x18\x06 \x03(\tR\x17leadingDetachedComments\"\xd0\x02\n\x11GeneratedCodeInfo\x12M\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.AnnotationR\nannotation\x1a\xeb\x01\n\nAnnotation\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x1f\n\x0bsource_file\x18\x02 \x01(\tR\nsourceFile\x12\x14\n\x05\x62\x65gin\x18\x03 \x01(\x05R\x05\x62\x65gin\x12\x10\n\x03\x65nd\x18\x04 \x01(\x05R\x03\x65nd\x12R\n\x08semantic\x18\x05 \x01(\x0e\x32\x36.google.protobuf.GeneratedCodeInfo.Annotation.SemanticR\x08semantic\"(\n\x08Semantic\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03SET\x10\x01\x12\t\n\x05\x41LIAS\x10\x02\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection' - ) -else: - DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"M\n\x11\x46ileDescriptorSet\x12\x38\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProtoR\x04\x66ile\"\xfe\x04\n\x13\x46ileDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n\x07package\x18\x02 \x01(\tR\x07package\x12\x1e\n\ndependency\x18\x03 \x03(\tR\ndependency\x12+\n\x11public_dependency\x18\n \x03(\x05R\x10publicDependency\x12\'\n\x0fweak_dependency\x18\x0b \x03(\x05R\x0eweakDependency\x12\x43\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\x0bmessageType\x12\x41\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12\x41\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProtoR\x07service\x12\x43\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x36\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptionsR\x07options\x12I\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfoR\x0esourceCodeInfo\x12\x16\n\x06syntax\x18\x0c \x01(\tR\x06syntax\x12\x18\n\x07\x65\x64ition\x18\r \x01(\tR\x07\x65\x64ition\"\xb9\x06\n\x0f\x44\x65scriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12;\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\x05\x66ield\x12\x43\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x41\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\nnestedType\x12\x41\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12X\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRangeR\x0e\x65xtensionRange\x12\x44\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProtoR\toneofDecl\x12\x39\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptionsR\x07options\x12U\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\n \x03(\tR\x0creservedName\x1az\n\x0e\x45xtensionRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\x12@\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptionsR\x07options\x1a\x37\n\rReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\xad\x04\n\x15\x45xtensionRangeOptions\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\x12Y\n\x0b\x64\x65\x63laration\x18\x02 \x03(\x0b\x32\x32.google.protobuf.ExtensionRangeOptions.DeclarationB\x03\x88\x01\x02R\x0b\x64\x65\x63laration\x12h\n\x0cverification\x18\x03 \x01(\x0e\x32\x38.google.protobuf.ExtensionRangeOptions.VerificationState:\nUNVERIFIEDR\x0cverification\x1a\xb3\x01\n\x0b\x44\x65\x63laration\x12\x16\n\x06number\x18\x01 \x01(\x05R\x06number\x12\x1b\n\tfull_name\x18\x02 \x01(\tR\x08\x66ullName\x12\x12\n\x04type\x18\x03 \x01(\tR\x04type\x12#\n\x0bis_repeated\x18\x04 \x01(\x08\x42\x02\x18\x01R\nisRepeated\x12\x1a\n\x08reserved\x18\x05 \x01(\x08R\x08reserved\x12\x1a\n\x08repeated\x18\x06 \x01(\x08R\x08repeated\"4\n\x11VerificationState\x12\x0f\n\x0b\x44\x45\x43LARATION\x10\x00\x12\x0e\n\nUNVERIFIED\x10\x01*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xc1\x06\n\x14\x46ieldDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x03 \x01(\x05R\x06number\x12\x41\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.LabelR\x05label\x12>\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.TypeR\x04type\x12\x1b\n\ttype_name\x18\x06 \x01(\tR\x08typeName\x12\x1a\n\x08\x65xtendee\x18\x02 \x01(\tR\x08\x65xtendee\x12#\n\rdefault_value\x18\x07 \x01(\tR\x0c\x64\x65\x66\x61ultValue\x12\x1f\n\x0boneof_index\x18\t \x01(\x05R\noneofIndex\x12\x1b\n\tjson_name\x18\n \x01(\tR\x08jsonName\x12\x37\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptionsR\x07options\x12\'\n\x0fproto3_optional\x18\x11 \x01(\x08R\x0eproto3Optional\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"c\n\x14OneofDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x37\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptionsR\x07options\"\xe3\x02\n\x13\x45numDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12?\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProtoR\x05value\x12\x36\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptionsR\x07options\x12]\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\x05 \x03(\tR\x0creservedName\x1a;\n\x11\x45numReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\x83\x01\n\x18\x45numValueDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x02 \x01(\x05R\x06number\x12;\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptionsR\x07options\"\xa7\x01\n\x16ServiceDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12>\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProtoR\x06method\x12\x39\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptionsR\x07options\"\x89\x02\n\x15MethodDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n\ninput_type\x18\x02 \x01(\tR\tinputType\x12\x1f\n\x0boutput_type\x18\x03 \x01(\tR\noutputType\x12\x38\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptionsR\x07options\x12\x30\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lseR\x0f\x63lientStreaming\x12\x30\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lseR\x0fserverStreaming\"\x91\t\n\x0b\x46ileOptions\x12!\n\x0cjava_package\x18\x01 \x01(\tR\x0bjavaPackage\x12\x30\n\x14java_outer_classname\x18\x08 \x01(\tR\x12javaOuterClassname\x12\x35\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lseR\x11javaMultipleFiles\x12\x44\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01R\x19javaGenerateEqualsAndHash\x12:\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lseR\x13javaStringCheckUtf8\x12S\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEEDR\x0boptimizeFor\x12\x1d\n\ngo_package\x18\x0b \x01(\tR\tgoPackage\x12\x35\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lseR\x11\x63\x63GenericServices\x12\x39\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lseR\x13javaGenericServices\x12\x35\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lseR\x11pyGenericServices\x12\x37\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lseR\x12phpGenericServices\x12%\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12.\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04trueR\x0e\x63\x63\x45nableArenas\x12*\n\x11objc_class_prefix\x18$ \x01(\tR\x0fobjcClassPrefix\x12)\n\x10\x63sharp_namespace\x18% \x01(\tR\x0f\x63sharpNamespace\x12!\n\x0cswift_prefix\x18\' \x01(\tR\x0bswiftPrefix\x12(\n\x10php_class_prefix\x18( \x01(\tR\x0ephpClassPrefix\x12#\n\rphp_namespace\x18) \x01(\tR\x0cphpNamespace\x12\x34\n\x16php_metadata_namespace\x18, \x01(\tR\x14phpMetadataNamespace\x12!\n\x0cruby_package\x18- \x01(\tR\x0brubyPackage\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\xbb\x03\n\x0eMessageOptions\x12<\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lseR\x14messageSetWireFormat\x12L\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lseR\x1cnoStandardDescriptorAccessor\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x1b\n\tmap_entry\x18\x07 \x01(\x08R\x08mapEntry\x12V\n&deprecated_legacy_json_field_conflicts\x18\x0b \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\x85\t\n\x0c\x46ieldOptions\x12\x41\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRINGR\x05\x63type\x12\x16\n\x06packed\x18\x02 \x01(\x08R\x06packed\x12G\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMALR\x06jstype\x12\x19\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lseR\x04lazy\x12.\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lseR\x0eunverifiedLazy\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x19\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lseR\x04weak\x12(\n\x0c\x64\x65\x62ug_redact\x18\x10 \x01(\x08:\x05\x66\x61lseR\x0b\x64\x65\x62ugRedact\x12K\n\tretention\x18\x11 \x01(\x0e\x32-.google.protobuf.FieldOptions.OptionRetentionR\tretention\x12J\n\x06target\x18\x12 \x01(\x0e\x32..google.protobuf.FieldOptions.OptionTargetTypeB\x02\x18\x01R\x06target\x12H\n\x07targets\x18\x13 \x03(\x0e\x32..google.protobuf.FieldOptions.OptionTargetTypeR\x07targets\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02\"U\n\x0fOptionRetention\x12\x15\n\x11RETENTION_UNKNOWN\x10\x00\x12\x15\n\x11RETENTION_RUNTIME\x10\x01\x12\x14\n\x10RETENTION_SOURCE\x10\x02\"\x8c\x02\n\x10OptionTargetType\x12\x17\n\x13TARGET_TYPE_UNKNOWN\x10\x00\x12\x14\n\x10TARGET_TYPE_FILE\x10\x01\x12\x1f\n\x1bTARGET_TYPE_EXTENSION_RANGE\x10\x02\x12\x17\n\x13TARGET_TYPE_MESSAGE\x10\x03\x12\x15\n\x11TARGET_TYPE_FIELD\x10\x04\x12\x15\n\x11TARGET_TYPE_ONEOF\x10\x05\x12\x14\n\x10TARGET_TYPE_ENUM\x10\x06\x12\x1a\n\x16TARGET_TYPE_ENUM_ENTRY\x10\x07\x12\x17\n\x13TARGET_TYPE_SERVICE\x10\x08\x12\x16\n\x12TARGET_TYPE_METHOD\x10\t*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"s\n\x0cOneofOptions\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x98\x02\n\x0b\x45numOptions\x12\x1f\n\x0b\x61llow_alias\x18\x02 \x01(\x08R\nallowAlias\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12V\n&deprecated_legacy_json_field_conflicts\x18\x06 \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"\x9e\x01\n\x10\x45numValueOptions\x12%\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9c\x01\n\x0eServiceOptions\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xe0\x02\n\rMethodOptions\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12q\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWNR\x10idempotencyLevel\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9a\x03\n\x13UninterpretedOption\x12\x41\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePartR\x04name\x12)\n\x10identifier_value\x18\x03 \x01(\tR\x0fidentifierValue\x12,\n\x12positive_int_value\x18\x04 \x01(\x04R\x10positiveIntValue\x12,\n\x12negative_int_value\x18\x05 \x01(\x03R\x10negativeIntValue\x12!\n\x0c\x64ouble_value\x18\x06 \x01(\x01R\x0b\x64oubleValue\x12!\n\x0cstring_value\x18\x07 \x01(\x0cR\x0bstringValue\x12\'\n\x0f\x61ggregate_value\x18\x08 \x01(\tR\x0e\x61ggregateValue\x1aJ\n\x08NamePart\x12\x1b\n\tname_part\x18\x01 \x02(\tR\x08namePart\x12!\n\x0cis_extension\x18\x02 \x02(\x08R\x0bisExtension\"\xa7\x02\n\x0eSourceCodeInfo\x12\x44\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.LocationR\x08location\x1a\xce\x01\n\x08Location\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x16\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01R\x04span\x12)\n\x10leading_comments\x18\x03 \x01(\tR\x0fleadingComments\x12+\n\x11trailing_comments\x18\x04 \x01(\tR\x10trailingComments\x12:\n\x19leading_detached_comments\x18\x06 \x03(\tR\x17leadingDetachedComments\"\xd0\x02\n\x11GeneratedCodeInfo\x12M\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.AnnotationR\nannotation\x1a\xeb\x01\n\nAnnotation\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x1f\n\x0bsource_file\x18\x02 \x01(\tR\nsourceFile\x12\x14\n\x05\x62\x65gin\x18\x03 \x01(\x05R\x05\x62\x65gin\x12\x10\n\x03\x65nd\x18\x04 \x01(\x05R\x03\x65nd\x12R\n\x08semantic\x18\x05 \x01(\x0e\x32\x36.google.protobuf.GeneratedCodeInfo.Annotation.SemanticR\x08semantic\"(\n\x08Semantic\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03SET\x10\x01\x12\t\n\x05\x41LIAS\x10\x02\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection') - -_globals = globals() -if _descriptor._USE_C_DESCRIPTORS == False: - _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE = _descriptor.EnumDescriptor( - name='VerificationState', - full_name='google.protobuf.ExtensionRangeOptions.VerificationState', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='DECLARATION', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UNVERIFIED', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE) - - _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor( - name='Type', - full_name='google.protobuf.FieldDescriptorProto.Type', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='TYPE_DOUBLE', index=0, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_FLOAT', index=1, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_INT64', index=2, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_UINT64', index=3, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_INT32', index=4, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_FIXED64', index=5, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_FIXED32', index=6, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_BOOL', index=7, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_STRING', index=8, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_GROUP', index=9, number=10, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_MESSAGE', index=10, number=11, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_BYTES', index=11, number=12, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_UINT32', index=12, number=13, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_ENUM', index=13, number=14, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_SFIXED32', index=14, number=15, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_SFIXED64', index=15, number=16, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_SINT32', index=16, number=17, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TYPE_SINT64', index=17, number=18, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE) - - _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor( - name='Label', - full_name='google.protobuf.FieldDescriptorProto.Label', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='LABEL_OPTIONAL', index=0, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LABEL_REQUIRED', index=1, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LABEL_REPEATED', index=2, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL) - - _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor( - name='OptimizeMode', - full_name='google.protobuf.FileOptions.OptimizeMode', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='SPEED', index=0, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='CODE_SIZE', index=1, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LITE_RUNTIME', index=2, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE) - - _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor( - name='CType', - full_name='google.protobuf.FieldOptions.CType', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='STRING', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='CORD', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='STRING_PIECE', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE) - - _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor( - name='JSType', - full_name='google.protobuf.FieldOptions.JSType', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='JS_NORMAL', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='JS_STRING', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='JS_NUMBER', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE) - - _FIELDOPTIONS_OPTIONRETENTION = _descriptor.EnumDescriptor( - name='OptionRetention', - full_name='google.protobuf.FieldOptions.OptionRetention', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='RETENTION_UNKNOWN', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='RETENTION_RUNTIME', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='RETENTION_SOURCE', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_OPTIONRETENTION) - - _FIELDOPTIONS_OPTIONTARGETTYPE = _descriptor.EnumDescriptor( - name='OptionTargetType', - full_name='google.protobuf.FieldOptions.OptionTargetType', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_UNKNOWN', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_FILE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_EXTENSION_RANGE', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_MESSAGE', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_FIELD', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_ONEOF', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_ENUM', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_ENUM_ENTRY', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_SERVICE', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TARGET_TYPE_METHOD', index=9, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_OPTIONTARGETTYPE) - - _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor( - name='IdempotencyLevel', - full_name='google.protobuf.MethodOptions.IdempotencyLevel', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='IDEMPOTENCY_UNKNOWN', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NO_SIDE_EFFECTS', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='IDEMPOTENT', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL) - - _GENERATEDCODEINFO_ANNOTATION_SEMANTIC = _descriptor.EnumDescriptor( - name='Semantic', - full_name='google.protobuf.GeneratedCodeInfo.Annotation.Semantic', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='NONE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SET', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ALIAS', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - ) - _sym_db.RegisterEnumDescriptor(_GENERATEDCODEINFO_ANNOTATION_SEMANTIC) - - - _FILEDESCRIPTORSET = _descriptor.Descriptor( - name='FileDescriptorSet', - full_name='google.protobuf.FileDescriptorSet', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='file', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _FILEDESCRIPTORPROTO = _descriptor.Descriptor( - name='FileDescriptorProto', - full_name='google.protobuf.FileDescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='package', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2, - number=3, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='dependency', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3, - number=10, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='publicDependency', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4, - number=11, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='weakDependency', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='messageType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='enumType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='service', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='extension', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='sourceCodeInfo', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11, - number=12, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='syntax', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='edition', full_name='google.protobuf.FileDescriptorProto.edition', index=12, - number=13, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='edition', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor( - name='ExtensionRange', - full_name='google.protobuf.DescriptorProto.ExtensionRange', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='start', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor( - name='ReservedRange', - full_name='google.protobuf.DescriptorProto.ReservedRange', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='start', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _DESCRIPTORPROTO = _descriptor.Descriptor( - name='DescriptorProto', - full_name='google.protobuf.DescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.DescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='field', full_name='google.protobuf.DescriptorProto.field', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='field', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='extension', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='nestedType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='enumType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='extensionRange', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6, - number=8, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='oneofDecl', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.DescriptorProto.options', index=7, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8, - number=9, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='reservedRange', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9, - number=10, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='reservedName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _EXTENSIONRANGEOPTIONS_DECLARATION = _descriptor.Descriptor( - name='Declaration', - full_name='google.protobuf.ExtensionRangeOptions.Declaration', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='number', full_name='google.protobuf.ExtensionRangeOptions.Declaration.number', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='number', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='full_name', full_name='google.protobuf.ExtensionRangeOptions.Declaration.full_name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='fullName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type', full_name='google.protobuf.ExtensionRangeOptions.Declaration.type', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='type', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_repeated', full_name='google.protobuf.ExtensionRangeOptions.Declaration.is_repeated', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='isRepeated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='reserved', full_name='google.protobuf.ExtensionRangeOptions.Declaration.reserved', index=4, - number=5, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='reserved', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='repeated', full_name='google.protobuf.ExtensionRangeOptions.Declaration.repeated', index=5, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='repeated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor( - name='ExtensionRangeOptions', - full_name='google.protobuf.ExtensionRangeOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='declaration', full_name='google.protobuf.ExtensionRangeOptions.declaration', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='declaration', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='verification', full_name='google.protobuf.ExtensionRangeOptions.verification', index=2, - number=3, type=14, cpp_type=8, label=1, - has_default_value=True, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='verification', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_EXTENSIONRANGEOPTIONS_DECLARATION, ], - enum_types=[ - _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE, - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _FIELDDESCRIPTORPROTO = _descriptor.Descriptor( - name='FieldDescriptorProto', - full_name='google.protobuf.FieldDescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='number', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2, - number=4, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='label', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='type', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='typeName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='extendee', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6, - number=7, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='defaultValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7, - number=9, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='oneofIndex', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8, - number=10, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='jsonName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10, - number=17, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='proto3Optional', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _FIELDDESCRIPTORPROTO_TYPE, - _FIELDDESCRIPTORPROTO_LABEL, - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor( - name='OneofDescriptorProto', - full_name='google.protobuf.OneofDescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor( - name='EnumReservedRange', - full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='start', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _ENUMDESCRIPTORPROTO = _descriptor.Descriptor( - name='EnumDescriptorProto', - full_name='google.protobuf.EnumDescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='value', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='reservedRange', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4, - number=5, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='reservedName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor( - name='EnumValueDescriptorProto', - full_name='google.protobuf.EnumValueDescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='number', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor( - name='ServiceDescriptorProto', - full_name='google.protobuf.ServiceDescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='method', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _METHODDESCRIPTORPROTO = _descriptor.Descriptor( - name='MethodDescriptorProto', - full_name='google.protobuf.MethodDescriptorProto', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='inputType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='outputType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4, - number=5, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='clientStreaming', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5, - number=6, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='serverStreaming', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _FILEOPTIONS = _descriptor.Descriptor( - name='FileOptions', - full_name='google.protobuf.FileOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='javaPackage', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1, - number=8, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='javaOuterClassname', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2, - number=10, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='javaMultipleFiles', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3, - number=20, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='javaGenerateEqualsAndHash', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4, - number=27, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='javaStringCheckUtf8', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5, - number=9, type=14, cpp_type=8, label=1, - has_default_value=True, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='optimizeFor', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6, - number=11, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='goPackage', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7, - number=16, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='ccGenericServices', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8, - number=17, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='javaGenericServices', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9, - number=18, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='pyGenericServices', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='php_generic_services', full_name='google.protobuf.FileOptions.php_generic_services', index=10, - number=42, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='phpGenericServices', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=11, - number=23, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=12, - number=31, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=True, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='ccEnableArenas', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=13, - number=36, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='objcClassPrefix', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=14, - number=37, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='csharpNamespace', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=15, - number=39, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='swiftPrefix', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=16, - number=40, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='phpClassPrefix', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=17, - number=41, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='phpNamespace', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=18, - number=44, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='phpMetadataNamespace', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=19, - number=45, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='rubyPackage', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _FILEOPTIONS_OPTIMIZEMODE, - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _MESSAGEOPTIONS = _descriptor.Descriptor( - name='MessageOptions', - full_name='google.protobuf.MessageOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0, - number=1, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='messageSetWireFormat', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='noStandardDescriptorAccessor', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3, - number=7, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='mapEntry', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='deprecated_legacy_json_field_conflicts', full_name='google.protobuf.MessageOptions.deprecated_legacy_json_field_conflicts', index=4, - number=11, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecatedLegacyJsonFieldConflicts', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=5, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _FIELDOPTIONS = _descriptor.Descriptor( - name='FieldOptions', - full_name='google.protobuf.FieldOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='ctype', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='packed', full_name='google.protobuf.FieldOptions.packed', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='packed', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2, - number=6, type=14, cpp_type=8, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='jstype', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3, - number=5, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='lazy', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4, - number=15, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='unverifiedLazy', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5, - number=3, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='weak', full_name='google.protobuf.FieldOptions.weak', index=6, - number=10, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='weak', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='debug_redact', full_name='google.protobuf.FieldOptions.debug_redact', index=7, - number=16, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='debugRedact', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='retention', full_name='google.protobuf.FieldOptions.retention', index=8, - number=17, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='retention', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='target', full_name='google.protobuf.FieldOptions.target', index=9, - number=18, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='target', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='targets', full_name='google.protobuf.FieldOptions.targets', index=10, - number=19, type=14, cpp_type=8, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='targets', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=11, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _FIELDOPTIONS_CTYPE, - _FIELDOPTIONS_JSTYPE, - _FIELDOPTIONS_OPTIONRETENTION, - _FIELDOPTIONS_OPTIONTARGETTYPE, - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _ONEOFOPTIONS = _descriptor.Descriptor( - name='OneofOptions', - full_name='google.protobuf.OneofOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=0, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _ENUMOPTIONS = _descriptor.Descriptor( - name='EnumOptions', - full_name='google.protobuf.EnumOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='allowAlias', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1, - number=3, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='deprecated_legacy_json_field_conflicts', full_name='google.protobuf.EnumOptions.deprecated_legacy_json_field_conflicts', index=2, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecatedLegacyJsonFieldConflicts', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=3, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _ENUMVALUEOPTIONS = _descriptor.Descriptor( - name='EnumValueOptions', - full_name='google.protobuf.EnumValueOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0, - number=1, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=1, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _SERVICEOPTIONS = _descriptor.Descriptor( - name='ServiceOptions', - full_name='google.protobuf.ServiceOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=0, - number=33, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=1, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _METHODOPTIONS = _descriptor.Descriptor( - name='MethodOptions', - full_name='google.protobuf.MethodOptions', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0, - number=33, type=8, cpp_type=7, label=1, - has_default_value=True, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1, - number=34, type=14, cpp_type=8, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='idempotencyLevel', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=2, - number=999, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _METHODOPTIONS_IDEMPOTENCYLEVEL, - ], - serialized_options=None, - is_extendable=True, - syntax='proto2', - extension_ranges=[(1000, 536870912), ], - oneofs=[ - ], - ) - - - _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor( - name='NamePart', - full_name='google.protobuf.UninterpretedOption.NamePart', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='namePart', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1, - number=2, type=8, cpp_type=7, label=2, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='isExtension', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _UNINTERPRETEDOPTION = _descriptor.Descriptor( - name='UninterpretedOption', - full_name='google.protobuf.UninterpretedOption', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='google.protobuf.UninterpretedOption.name', index=0, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='identifierValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2, - number=4, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='positiveIntValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3, - number=5, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='negativeIntValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4, - number=6, type=1, cpp_type=5, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='doubleValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5, - number=7, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='stringValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6, - number=8, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='aggregateValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _SOURCECODEINFO_LOCATION = _descriptor.Descriptor( - name='Location', - full_name='google.protobuf.SourceCodeInfo.Location', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0, - number=1, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='path', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1, - number=2, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='span', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='leadingComments', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='trailingComments', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4, - number=6, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='leadingDetachedComments', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _SOURCECODEINFO = _descriptor.Descriptor( - name='SourceCodeInfo', - full_name='google.protobuf.SourceCodeInfo', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='location', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_SOURCECODEINFO_LOCATION, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - - _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor( - name='Annotation', - full_name='google.protobuf.GeneratedCodeInfo.Annotation', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0, - number=1, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='path', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='sourceFile', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='begin', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3, - number=4, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='semantic', full_name='google.protobuf.GeneratedCodeInfo.Annotation.semantic', index=4, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='semantic', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _GENERATEDCODEINFO_ANNOTATION_SEMANTIC, - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _GENERATEDCODEINFO = _descriptor.Descriptor( - name='GeneratedCodeInfo', - full_name='google.protobuf.GeneratedCodeInfo', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, json_name='annotation', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_GENERATEDCODEINFO_ANNOTATION, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - ) - - _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO - _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO - _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO - _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO - _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO - _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS - _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO - _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS - _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO - _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO - _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO - _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO - _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO - _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO - _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE - _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO - _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS - _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE - _EXTENSIONRANGEOPTIONS_DECLARATION.containing_type = _EXTENSIONRANGEOPTIONS - _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _EXTENSIONRANGEOPTIONS.fields_by_name['declaration'].message_type = _EXTENSIONRANGEOPTIONS_DECLARATION - _EXTENSIONRANGEOPTIONS.fields_by_name['verification'].enum_type = _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE - _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE.containing_type = _EXTENSIONRANGEOPTIONS - _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL - _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE - _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS - _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO - _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO - _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS - _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO - _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO - _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS - _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE - _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS - _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO - _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS - _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS - _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE - _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS - _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE - _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE - _FIELDOPTIONS.fields_by_name['retention'].enum_type = _FIELDOPTIONS_OPTIONRETENTION - _FIELDOPTIONS.fields_by_name['target'].enum_type = _FIELDOPTIONS_OPTIONTARGETTYPE - _FIELDOPTIONS.fields_by_name['targets'].enum_type = _FIELDOPTIONS_OPTIONTARGETTYPE - _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS - _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS - _FIELDOPTIONS_OPTIONRETENTION.containing_type = _FIELDOPTIONS - _FIELDOPTIONS_OPTIONTARGETTYPE.containing_type = _FIELDOPTIONS - _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL - _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION - _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS - _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION - _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART - _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO - _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION - _GENERATEDCODEINFO_ANNOTATION.fields_by_name['semantic'].enum_type = _GENERATEDCODEINFO_ANNOTATION_SEMANTIC - _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO - _GENERATEDCODEINFO_ANNOTATION_SEMANTIC.containing_type = _GENERATEDCODEINFO_ANNOTATION - _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION - DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET - DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS - DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO - DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS - DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS - DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS - DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS - DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS - DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS - DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS - DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS - DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION - DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO - DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO - _sym_db.RegisterFileDescriptor(DESCRIPTOR) - -else: - _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _globals['_FILEDESCRIPTORSET']._serialized_start=53 - _globals['_FILEDESCRIPTORSET']._serialized_end=130 - _globals['_FILEDESCRIPTORPROTO']._serialized_start=133 - _globals['_FILEDESCRIPTORPROTO']._serialized_end=771 - _globals['_DESCRIPTORPROTO']._serialized_start=774 - _globals['_DESCRIPTORPROTO']._serialized_end=1599 - _globals['_DESCRIPTORPROTO_EXTENSIONRANGE']._serialized_start=1420 - _globals['_DESCRIPTORPROTO_EXTENSIONRANGE']._serialized_end=1542 - _globals['_DESCRIPTORPROTO_RESERVEDRANGE']._serialized_start=1544 - _globals['_DESCRIPTORPROTO_RESERVEDRANGE']._serialized_end=1599 - _globals['_EXTENSIONRANGEOPTIONS']._serialized_start=1602 - _globals['_EXTENSIONRANGEOPTIONS']._serialized_end=2159 - _globals['_EXTENSIONRANGEOPTIONS_DECLARATION']._serialized_start=1915 - _globals['_EXTENSIONRANGEOPTIONS_DECLARATION']._serialized_end=2094 - _globals['_EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE']._serialized_start=2096 - _globals['_EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE']._serialized_end=2148 - _globals['_FIELDDESCRIPTORPROTO']._serialized_start=2162 - _globals['_FIELDDESCRIPTORPROTO']._serialized_end=2995 - _globals['_FIELDDESCRIPTORPROTO_TYPE']._serialized_start=2616 - _globals['_FIELDDESCRIPTORPROTO_TYPE']._serialized_end=2926 - _globals['_FIELDDESCRIPTORPROTO_LABEL']._serialized_start=2928 - _globals['_FIELDDESCRIPTORPROTO_LABEL']._serialized_end=2995 - _globals['_ONEOFDESCRIPTORPROTO']._serialized_start=2997 - _globals['_ONEOFDESCRIPTORPROTO']._serialized_end=3096 - _globals['_ENUMDESCRIPTORPROTO']._serialized_start=3099 - _globals['_ENUMDESCRIPTORPROTO']._serialized_end=3454 - _globals['_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE']._serialized_start=3395 - _globals['_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE']._serialized_end=3454 - _globals['_ENUMVALUEDESCRIPTORPROTO']._serialized_start=3457 - _globals['_ENUMVALUEDESCRIPTORPROTO']._serialized_end=3588 - _globals['_SERVICEDESCRIPTORPROTO']._serialized_start=3591 - _globals['_SERVICEDESCRIPTORPROTO']._serialized_end=3758 - _globals['_METHODDESCRIPTORPROTO']._serialized_start=3761 - _globals['_METHODDESCRIPTORPROTO']._serialized_end=4026 - _globals['_FILEOPTIONS']._serialized_start=4029 - _globals['_FILEOPTIONS']._serialized_end=5198 - _globals['_FILEOPTIONS_OPTIMIZEMODE']._serialized_start=5123 - _globals['_FILEOPTIONS_OPTIMIZEMODE']._serialized_end=5181 - _globals['_MESSAGEOPTIONS']._serialized_start=5201 - _globals['_MESSAGEOPTIONS']._serialized_end=5644 - _globals['_FIELDOPTIONS']._serialized_start=5647 - _globals['_FIELDOPTIONS']._serialized_end=6804 - _globals['_FIELDOPTIONS_CTYPE']._serialized_start=6327 - _globals['_FIELDOPTIONS_CTYPE']._serialized_end=6374 - _globals['_FIELDOPTIONS_JSTYPE']._serialized_start=6376 - _globals['_FIELDOPTIONS_JSTYPE']._serialized_end=6429 - _globals['_FIELDOPTIONS_OPTIONRETENTION']._serialized_start=6431 - _globals['_FIELDOPTIONS_OPTIONRETENTION']._serialized_end=6516 - _globals['_FIELDOPTIONS_OPTIONTARGETTYPE']._serialized_start=6519 - _globals['_FIELDOPTIONS_OPTIONTARGETTYPE']._serialized_end=6787 - _globals['_ONEOFOPTIONS']._serialized_start=6806 - _globals['_ONEOFOPTIONS']._serialized_end=6921 - _globals['_ENUMOPTIONS']._serialized_start=6924 - _globals['_ENUMOPTIONS']._serialized_end=7204 - _globals['_ENUMVALUEOPTIONS']._serialized_start=7207 - _globals['_ENUMVALUEOPTIONS']._serialized_end=7365 - _globals['_SERVICEOPTIONS']._serialized_start=7368 - _globals['_SERVICEOPTIONS']._serialized_end=7524 - _globals['_METHODOPTIONS']._serialized_start=7527 - _globals['_METHODOPTIONS']._serialized_end=7879 - _globals['_METHODOPTIONS_IDEMPOTENCYLEVEL']._serialized_start=7788 - _globals['_METHODOPTIONS_IDEMPOTENCYLEVEL']._serialized_end=7868 - _globals['_UNINTERPRETEDOPTION']._serialized_start=7882 - _globals['_UNINTERPRETEDOPTION']._serialized_end=8292 - _globals['_UNINTERPRETEDOPTION_NAMEPART']._serialized_start=8218 - _globals['_UNINTERPRETEDOPTION_NAMEPART']._serialized_end=8292 - _globals['_SOURCECODEINFO']._serialized_start=8295 - _globals['_SOURCECODEINFO']._serialized_end=8590 - _globals['_SOURCECODEINFO_LOCATION']._serialized_start=8384 - _globals['_SOURCECODEINFO_LOCATION']._serialized_end=8590 - _globals['_GENERATEDCODEINFO']._serialized_start=8593 - _globals['_GENERATEDCODEINFO']._serialized_end=8929 - _globals['_GENERATEDCODEINFO_ANNOTATION']._serialized_start=8694 - _globals['_GENERATEDCODEINFO_ANNOTATION']._serialized_end=8929 - _globals['_GENERATEDCODEINFO_ANNOTATION_SEMANTIC']._serialized_start=8889 - _globals['_GENERATEDCODEINFO_ANNOTATION_SEMANTIC']._serialized_end=8929 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/descriptor_pool.py b/script.module.protobuf/resources/lib/google/protobuf/descriptor_pool.py deleted file mode 100644 index 1ebf11834..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/descriptor_pool.py +++ /dev/null @@ -1,1290 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Provides DescriptorPool to use as a container for proto2 descriptors. - -The DescriptorPool is used in conjection with a DescriptorDatabase to maintain -a collection of protocol buffer descriptors for use when dynamically creating -message types at runtime. - -For most applications protocol buffers should be used via modules generated by -the protocol buffer compiler tool. This should only be used when the type of -protocol buffers used in an application or library cannot be predetermined. - -Below is a straightforward example on how to use this class:: - - pool = DescriptorPool() - file_descriptor_protos = [ ... ] - for file_descriptor_proto in file_descriptor_protos: - pool.Add(file_descriptor_proto) - my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType') - -The message descriptor can be used in conjunction with the message_factory -module in order to create a protocol buffer class that can be encoded and -decoded. - -If you want to get a Python class for the specified proto, use the -helper functions inside google.protobuf.message_factory -directly instead of this class. -""" - -__author__ = 'matthewtoia@google.com (Matt Toia)' - -import collections -import warnings - -from google.protobuf import descriptor -from google.protobuf import descriptor_database -from google.protobuf import text_encoding - - -_USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS # pylint: disable=protected-access - - -def _Deprecated(func): - """Mark functions as deprecated.""" - - def NewFunc(*args, **kwargs): - warnings.warn( - 'Call to deprecated function %s(). Note: Do add unlinked descriptors ' - 'to descriptor_pool is wrong. Use Add() or AddSerializedFile() ' - 'instead.' % func.__name__, - category=DeprecationWarning) - return func(*args, **kwargs) - NewFunc.__name__ = func.__name__ - NewFunc.__doc__ = func.__doc__ - NewFunc.__dict__.update(func.__dict__) - return NewFunc - - -def _NormalizeFullyQualifiedName(name): - """Remove leading period from fully-qualified type name. - - Due to b/13860351 in descriptor_database.py, types in the root namespace are - generated with a leading period. This function removes that prefix. - - Args: - name (str): The fully-qualified symbol name. - - Returns: - str: The normalized fully-qualified symbol name. - """ - return name.lstrip('.') - - -def _OptionsOrNone(descriptor_proto): - """Returns the value of the field `options`, or None if it is not set.""" - if descriptor_proto.HasField('options'): - return descriptor_proto.options - else: - return None - - -def _IsMessageSetExtension(field): - return (field.is_extension and - field.containing_type.has_options and - field.containing_type.GetOptions().message_set_wire_format and - field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and - field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL) - - -class DescriptorPool(object): - """A collection of protobufs dynamically constructed by descriptor protos.""" - - if _USE_C_DESCRIPTORS: - - def __new__(cls, descriptor_db=None): - # pylint: disable=protected-access - return descriptor._message.DescriptorPool(descriptor_db) - - def __init__( - self, descriptor_db=None, use_deprecated_legacy_json_field_conflicts=False - ): - """Initializes a Pool of proto buffs. - - The descriptor_db argument to the constructor is provided to allow - specialized file descriptor proto lookup code to be triggered on demand. An - example would be an implementation which will read and compile a file - specified in a call to FindFileByName() and not require the call to Add() - at all. Results from this database will be cached internally here as well. - - Args: - descriptor_db: A secondary source of file descriptors. - use_deprecated_legacy_json_field_conflicts: Unused, for compatibility with - C++. - """ - - self._internal_db = descriptor_database.DescriptorDatabase() - self._descriptor_db = descriptor_db - self._descriptors = {} - self._enum_descriptors = {} - self._service_descriptors = {} - self._file_descriptors = {} - self._toplevel_extensions = {} - self._top_enum_values = {} - # We store extensions in two two-level mappings: The first key is the - # descriptor of the message being extended, the second key is the extension - # full name or its tag number. - self._extensions_by_name = collections.defaultdict(dict) - self._extensions_by_number = collections.defaultdict(dict) - - def _CheckConflictRegister(self, desc, desc_name, file_name): - """Check if the descriptor name conflicts with another of the same name. - - Args: - desc: Descriptor of a message, enum, service, extension or enum value. - desc_name (str): the full name of desc. - file_name (str): The file name of descriptor. - """ - for register, descriptor_type in [ - (self._descriptors, descriptor.Descriptor), - (self._enum_descriptors, descriptor.EnumDescriptor), - (self._service_descriptors, descriptor.ServiceDescriptor), - (self._toplevel_extensions, descriptor.FieldDescriptor), - (self._top_enum_values, descriptor.EnumValueDescriptor)]: - if desc_name in register: - old_desc = register[desc_name] - if isinstance(old_desc, descriptor.EnumValueDescriptor): - old_file = old_desc.type.file.name - else: - old_file = old_desc.file.name - - if not isinstance(desc, descriptor_type) or ( - old_file != file_name): - error_msg = ('Conflict register for file "' + file_name + - '": ' + desc_name + - ' is already defined in file "' + - old_file + '". Please fix the conflict by adding ' - 'package name on the proto file, or use different ' - 'name for the duplication.') - if isinstance(desc, descriptor.EnumValueDescriptor): - error_msg += ('\nNote: enum values appear as ' - 'siblings of the enum type instead of ' - 'children of it.') - - raise TypeError(error_msg) - - return - - def Add(self, file_desc_proto): - """Adds the FileDescriptorProto and its types to this pool. - - Args: - file_desc_proto (FileDescriptorProto): The file descriptor to add. - """ - - self._internal_db.Add(file_desc_proto) - - def AddSerializedFile(self, serialized_file_desc_proto): - """Adds the FileDescriptorProto and its types to this pool. - - Args: - serialized_file_desc_proto (bytes): A bytes string, serialization of the - :class:`FileDescriptorProto` to add. - - Returns: - FileDescriptor: Descriptor for the added file. - """ - - # pylint: disable=g-import-not-at-top - from google.protobuf import descriptor_pb2 - file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString( - serialized_file_desc_proto) - file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto) - file_desc.serialized_pb = serialized_file_desc_proto - return file_desc - - # Add Descriptor to descriptor pool is deprecated. Please use Add() - # or AddSerializedFile() to add a FileDescriptorProto instead. - @_Deprecated - def AddDescriptor(self, desc): - self._AddDescriptor(desc) - - # Never call this method. It is for internal usage only. - def _AddDescriptor(self, desc): - """Adds a Descriptor to the pool, non-recursively. - - If the Descriptor contains nested messages or enums, the caller must - explicitly register them. This method also registers the FileDescriptor - associated with the message. - - Args: - desc: A Descriptor. - """ - if not isinstance(desc, descriptor.Descriptor): - raise TypeError('Expected instance of descriptor.Descriptor.') - - self._CheckConflictRegister(desc, desc.full_name, desc.file.name) - - self._descriptors[desc.full_name] = desc - self._AddFileDescriptor(desc.file) - - # Add EnumDescriptor to descriptor pool is deprecated. Please use Add() - # or AddSerializedFile() to add a FileDescriptorProto instead. - @_Deprecated - def AddEnumDescriptor(self, enum_desc): - self._AddEnumDescriptor(enum_desc) - - # Never call this method. It is for internal usage only. - def _AddEnumDescriptor(self, enum_desc): - """Adds an EnumDescriptor to the pool. - - This method also registers the FileDescriptor associated with the enum. - - Args: - enum_desc: An EnumDescriptor. - """ - - if not isinstance(enum_desc, descriptor.EnumDescriptor): - raise TypeError('Expected instance of descriptor.EnumDescriptor.') - - file_name = enum_desc.file.name - self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name) - self._enum_descriptors[enum_desc.full_name] = enum_desc - - # Top enum values need to be indexed. - # Count the number of dots to see whether the enum is toplevel or nested - # in a message. We cannot use enum_desc.containing_type at this stage. - if enum_desc.file.package: - top_level = (enum_desc.full_name.count('.') - - enum_desc.file.package.count('.') == 1) - else: - top_level = enum_desc.full_name.count('.') == 0 - if top_level: - file_name = enum_desc.file.name - package = enum_desc.file.package - for enum_value in enum_desc.values: - full_name = _NormalizeFullyQualifiedName( - '.'.join((package, enum_value.name))) - self._CheckConflictRegister(enum_value, full_name, file_name) - self._top_enum_values[full_name] = enum_value - self._AddFileDescriptor(enum_desc.file) - - # Add ServiceDescriptor to descriptor pool is deprecated. Please use Add() - # or AddSerializedFile() to add a FileDescriptorProto instead. - @_Deprecated - def AddServiceDescriptor(self, service_desc): - self._AddServiceDescriptor(service_desc) - - # Never call this method. It is for internal usage only. - def _AddServiceDescriptor(self, service_desc): - """Adds a ServiceDescriptor to the pool. - - Args: - service_desc: A ServiceDescriptor. - """ - - if not isinstance(service_desc, descriptor.ServiceDescriptor): - raise TypeError('Expected instance of descriptor.ServiceDescriptor.') - - self._CheckConflictRegister(service_desc, service_desc.full_name, - service_desc.file.name) - self._service_descriptors[service_desc.full_name] = service_desc - - # Add ExtensionDescriptor to descriptor pool is deprecated. Please use Add() - # or AddSerializedFile() to add a FileDescriptorProto instead. - @_Deprecated - def AddExtensionDescriptor(self, extension): - self._AddExtensionDescriptor(extension) - - # Never call this method. It is for internal usage only. - def _AddExtensionDescriptor(self, extension): - """Adds a FieldDescriptor describing an extension to the pool. - - Args: - extension: A FieldDescriptor. - - Raises: - AssertionError: when another extension with the same number extends the - same message. - TypeError: when the specified extension is not a - descriptor.FieldDescriptor. - """ - if not (isinstance(extension, descriptor.FieldDescriptor) and - extension.is_extension): - raise TypeError('Expected an extension descriptor.') - - if extension.extension_scope is None: - self._CheckConflictRegister( - extension, extension.full_name, extension.file.name) - self._toplevel_extensions[extension.full_name] = extension - - try: - existing_desc = self._extensions_by_number[ - extension.containing_type][extension.number] - except KeyError: - pass - else: - if extension is not existing_desc: - raise AssertionError( - 'Extensions "%s" and "%s" both try to extend message type "%s" ' - 'with field number %d.' % - (extension.full_name, existing_desc.full_name, - extension.containing_type.full_name, extension.number)) - - self._extensions_by_number[extension.containing_type][ - extension.number] = extension - self._extensions_by_name[extension.containing_type][ - extension.full_name] = extension - - # Also register MessageSet extensions with the type name. - if _IsMessageSetExtension(extension): - self._extensions_by_name[extension.containing_type][ - extension.message_type.full_name] = extension - - @_Deprecated - def AddFileDescriptor(self, file_desc): - self._InternalAddFileDescriptor(file_desc) - - # Never call this method. It is for internal usage only. - def _InternalAddFileDescriptor(self, file_desc): - """Adds a FileDescriptor to the pool, non-recursively. - - If the FileDescriptor contains messages or enums, the caller must explicitly - register them. - - Args: - file_desc: A FileDescriptor. - """ - - self._AddFileDescriptor(file_desc) - - def _AddFileDescriptor(self, file_desc): - """Adds a FileDescriptor to the pool, non-recursively. - - If the FileDescriptor contains messages or enums, the caller must explicitly - register them. - - Args: - file_desc: A FileDescriptor. - """ - - if not isinstance(file_desc, descriptor.FileDescriptor): - raise TypeError('Expected instance of descriptor.FileDescriptor.') - self._file_descriptors[file_desc.name] = file_desc - - def FindFileByName(self, file_name): - """Gets a FileDescriptor by file name. - - Args: - file_name (str): The path to the file to get a descriptor for. - - Returns: - FileDescriptor: The descriptor for the named file. - - Raises: - KeyError: if the file cannot be found in the pool. - """ - - try: - return self._file_descriptors[file_name] - except KeyError: - pass - - try: - file_proto = self._internal_db.FindFileByName(file_name) - except KeyError as error: - if self._descriptor_db: - file_proto = self._descriptor_db.FindFileByName(file_name) - else: - raise error - if not file_proto: - raise KeyError('Cannot find a file named %s' % file_name) - return self._ConvertFileProtoToFileDescriptor(file_proto) - - def FindFileContainingSymbol(self, symbol): - """Gets the FileDescriptor for the file containing the specified symbol. - - Args: - symbol (str): The name of the symbol to search for. - - Returns: - FileDescriptor: Descriptor for the file that contains the specified - symbol. - - Raises: - KeyError: if the file cannot be found in the pool. - """ - - symbol = _NormalizeFullyQualifiedName(symbol) - try: - return self._InternalFindFileContainingSymbol(symbol) - except KeyError: - pass - - try: - # Try fallback database. Build and find again if possible. - self._FindFileContainingSymbolInDb(symbol) - return self._InternalFindFileContainingSymbol(symbol) - except KeyError: - raise KeyError('Cannot find a file containing %s' % symbol) - - def _InternalFindFileContainingSymbol(self, symbol): - """Gets the already built FileDescriptor containing the specified symbol. - - Args: - symbol (str): The name of the symbol to search for. - - Returns: - FileDescriptor: Descriptor for the file that contains the specified - symbol. - - Raises: - KeyError: if the file cannot be found in the pool. - """ - try: - return self._descriptors[symbol].file - except KeyError: - pass - - try: - return self._enum_descriptors[symbol].file - except KeyError: - pass - - try: - return self._service_descriptors[symbol].file - except KeyError: - pass - - try: - return self._top_enum_values[symbol].type.file - except KeyError: - pass - - try: - return self._toplevel_extensions[symbol].file - except KeyError: - pass - - # Try fields, enum values and nested extensions inside a message. - top_name, _, sub_name = symbol.rpartition('.') - try: - message = self.FindMessageTypeByName(top_name) - assert (sub_name in message.extensions_by_name or - sub_name in message.fields_by_name or - sub_name in message.enum_values_by_name) - return message.file - except (KeyError, AssertionError): - raise KeyError('Cannot find a file containing %s' % symbol) - - def FindMessageTypeByName(self, full_name): - """Loads the named descriptor from the pool. - - Args: - full_name (str): The full name of the descriptor to load. - - Returns: - Descriptor: The descriptor for the named type. - - Raises: - KeyError: if the message cannot be found in the pool. - """ - - full_name = _NormalizeFullyQualifiedName(full_name) - if full_name not in self._descriptors: - self._FindFileContainingSymbolInDb(full_name) - return self._descriptors[full_name] - - def FindEnumTypeByName(self, full_name): - """Loads the named enum descriptor from the pool. - - Args: - full_name (str): The full name of the enum descriptor to load. - - Returns: - EnumDescriptor: The enum descriptor for the named type. - - Raises: - KeyError: if the enum cannot be found in the pool. - """ - - full_name = _NormalizeFullyQualifiedName(full_name) - if full_name not in self._enum_descriptors: - self._FindFileContainingSymbolInDb(full_name) - return self._enum_descriptors[full_name] - - def FindFieldByName(self, full_name): - """Loads the named field descriptor from the pool. - - Args: - full_name (str): The full name of the field descriptor to load. - - Returns: - FieldDescriptor: The field descriptor for the named field. - - Raises: - KeyError: if the field cannot be found in the pool. - """ - full_name = _NormalizeFullyQualifiedName(full_name) - message_name, _, field_name = full_name.rpartition('.') - message_descriptor = self.FindMessageTypeByName(message_name) - return message_descriptor.fields_by_name[field_name] - - def FindOneofByName(self, full_name): - """Loads the named oneof descriptor from the pool. - - Args: - full_name (str): The full name of the oneof descriptor to load. - - Returns: - OneofDescriptor: The oneof descriptor for the named oneof. - - Raises: - KeyError: if the oneof cannot be found in the pool. - """ - full_name = _NormalizeFullyQualifiedName(full_name) - message_name, _, oneof_name = full_name.rpartition('.') - message_descriptor = self.FindMessageTypeByName(message_name) - return message_descriptor.oneofs_by_name[oneof_name] - - def FindExtensionByName(self, full_name): - """Loads the named extension descriptor from the pool. - - Args: - full_name (str): The full name of the extension descriptor to load. - - Returns: - FieldDescriptor: The field descriptor for the named extension. - - Raises: - KeyError: if the extension cannot be found in the pool. - """ - full_name = _NormalizeFullyQualifiedName(full_name) - try: - # The proto compiler does not give any link between the FileDescriptor - # and top-level extensions unless the FileDescriptorProto is added to - # the DescriptorDatabase, but this can impact memory usage. - # So we registered these extensions by name explicitly. - return self._toplevel_extensions[full_name] - except KeyError: - pass - message_name, _, extension_name = full_name.rpartition('.') - try: - # Most extensions are nested inside a message. - scope = self.FindMessageTypeByName(message_name) - except KeyError: - # Some extensions are defined at file scope. - scope = self._FindFileContainingSymbolInDb(full_name) - return scope.extensions_by_name[extension_name] - - def FindExtensionByNumber(self, message_descriptor, number): - """Gets the extension of the specified message with the specified number. - - Extensions have to be registered to this pool by calling :func:`Add` or - :func:`AddExtensionDescriptor`. - - Args: - message_descriptor (Descriptor): descriptor of the extended message. - number (int): Number of the extension field. - - Returns: - FieldDescriptor: The descriptor for the extension. - - Raises: - KeyError: when no extension with the given number is known for the - specified message. - """ - try: - return self._extensions_by_number[message_descriptor][number] - except KeyError: - self._TryLoadExtensionFromDB(message_descriptor, number) - return self._extensions_by_number[message_descriptor][number] - - def FindAllExtensions(self, message_descriptor): - """Gets all the known extensions of a given message. - - Extensions have to be registered to this pool by build related - :func:`Add` or :func:`AddExtensionDescriptor`. - - Args: - message_descriptor (Descriptor): Descriptor of the extended message. - - Returns: - list[FieldDescriptor]: Field descriptors describing the extensions. - """ - # Fallback to descriptor db if FindAllExtensionNumbers is provided. - if self._descriptor_db and hasattr( - self._descriptor_db, 'FindAllExtensionNumbers'): - full_name = message_descriptor.full_name - all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name) - for number in all_numbers: - if number in self._extensions_by_number[message_descriptor]: - continue - self._TryLoadExtensionFromDB(message_descriptor, number) - - return list(self._extensions_by_number[message_descriptor].values()) - - def _TryLoadExtensionFromDB(self, message_descriptor, number): - """Try to Load extensions from descriptor db. - - Args: - message_descriptor: descriptor of the extended message. - number: the extension number that needs to be loaded. - """ - if not self._descriptor_db: - return - # Only supported when FindFileContainingExtension is provided. - if not hasattr( - self._descriptor_db, 'FindFileContainingExtension'): - return - - full_name = message_descriptor.full_name - file_proto = self._descriptor_db.FindFileContainingExtension( - full_name, number) - - if file_proto is None: - return - - try: - self._ConvertFileProtoToFileDescriptor(file_proto) - except: - warn_msg = ('Unable to load proto file %s for extension number %d.' % - (file_proto.name, number)) - warnings.warn(warn_msg, RuntimeWarning) - - def FindServiceByName(self, full_name): - """Loads the named service descriptor from the pool. - - Args: - full_name (str): The full name of the service descriptor to load. - - Returns: - ServiceDescriptor: The service descriptor for the named service. - - Raises: - KeyError: if the service cannot be found in the pool. - """ - full_name = _NormalizeFullyQualifiedName(full_name) - if full_name not in self._service_descriptors: - self._FindFileContainingSymbolInDb(full_name) - return self._service_descriptors[full_name] - - def FindMethodByName(self, full_name): - """Loads the named service method descriptor from the pool. - - Args: - full_name (str): The full name of the method descriptor to load. - - Returns: - MethodDescriptor: The method descriptor for the service method. - - Raises: - KeyError: if the method cannot be found in the pool. - """ - full_name = _NormalizeFullyQualifiedName(full_name) - service_name, _, method_name = full_name.rpartition('.') - service_descriptor = self.FindServiceByName(service_name) - return service_descriptor.methods_by_name[method_name] - - def _FindFileContainingSymbolInDb(self, symbol): - """Finds the file in descriptor DB containing the specified symbol. - - Args: - symbol (str): The name of the symbol to search for. - - Returns: - FileDescriptor: The file that contains the specified symbol. - - Raises: - KeyError: if the file cannot be found in the descriptor database. - """ - try: - file_proto = self._internal_db.FindFileContainingSymbol(symbol) - except KeyError as error: - if self._descriptor_db: - file_proto = self._descriptor_db.FindFileContainingSymbol(symbol) - else: - raise error - if not file_proto: - raise KeyError('Cannot find a file containing %s' % symbol) - return self._ConvertFileProtoToFileDescriptor(file_proto) - - def _ConvertFileProtoToFileDescriptor(self, file_proto): - """Creates a FileDescriptor from a proto or returns a cached copy. - - This method also has the side effect of loading all the symbols found in - the file into the appropriate dictionaries in the pool. - - Args: - file_proto: The proto to convert. - - Returns: - A FileDescriptor matching the passed in proto. - """ - if file_proto.name not in self._file_descriptors: - built_deps = list(self._GetDeps(file_proto.dependency)) - direct_deps = [self.FindFileByName(n) for n in file_proto.dependency] - public_deps = [direct_deps[i] for i in file_proto.public_dependency] - - file_descriptor = descriptor.FileDescriptor( - pool=self, - name=file_proto.name, - package=file_proto.package, - syntax=file_proto.syntax, - options=_OptionsOrNone(file_proto), - serialized_pb=file_proto.SerializeToString(), - dependencies=direct_deps, - public_dependencies=public_deps, - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - scope = {} - - # This loop extracts all the message and enum types from all the - # dependencies of the file_proto. This is necessary to create the - # scope of available message types when defining the passed in - # file proto. - for dependency in built_deps: - scope.update(self._ExtractSymbols( - dependency.message_types_by_name.values())) - scope.update((_PrefixWithDot(enum.full_name), enum) - for enum in dependency.enum_types_by_name.values()) - - for message_type in file_proto.message_type: - message_desc = self._ConvertMessageDescriptor( - message_type, file_proto.package, file_descriptor, scope, - file_proto.syntax) - file_descriptor.message_types_by_name[message_desc.name] = ( - message_desc) - - for enum_type in file_proto.enum_type: - file_descriptor.enum_types_by_name[enum_type.name] = ( - self._ConvertEnumDescriptor(enum_type, file_proto.package, - file_descriptor, None, scope, True)) - - for index, extension_proto in enumerate(file_proto.extension): - extension_desc = self._MakeFieldDescriptor( - extension_proto, file_proto.package, index, file_descriptor, - is_extension=True) - extension_desc.containing_type = self._GetTypeFromScope( - file_descriptor.package, extension_proto.extendee, scope) - self._SetFieldType(extension_proto, extension_desc, - file_descriptor.package, scope) - file_descriptor.extensions_by_name[extension_desc.name] = ( - extension_desc) - - for desc_proto in file_proto.message_type: - self._SetAllFieldTypes(file_proto.package, desc_proto, scope) - - if file_proto.package: - desc_proto_prefix = _PrefixWithDot(file_proto.package) - else: - desc_proto_prefix = '' - - for desc_proto in file_proto.message_type: - desc = self._GetTypeFromScope( - desc_proto_prefix, desc_proto.name, scope) - file_descriptor.message_types_by_name[desc_proto.name] = desc - - for index, service_proto in enumerate(file_proto.service): - file_descriptor.services_by_name[service_proto.name] = ( - self._MakeServiceDescriptor(service_proto, index, scope, - file_proto.package, file_descriptor)) - - self._file_descriptors[file_proto.name] = file_descriptor - - # Add extensions to the pool - file_desc = self._file_descriptors[file_proto.name] - for extension in file_desc.extensions_by_name.values(): - self._AddExtensionDescriptor(extension) - for message_type in file_desc.message_types_by_name.values(): - for extension in message_type.extensions: - self._AddExtensionDescriptor(extension) - - return file_desc - - def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None, - scope=None, syntax=None): - """Adds the proto to the pool in the specified package. - - Args: - desc_proto: The descriptor_pb2.DescriptorProto protobuf message. - package: The package the proto should be located in. - file_desc: The file containing this message. - scope: Dict mapping short and full symbols to message and enum types. - syntax: string indicating syntax of the file ("proto2" or "proto3") - - Returns: - The added descriptor. - """ - - if package: - desc_name = '.'.join((package, desc_proto.name)) - else: - desc_name = desc_proto.name - - if file_desc is None: - file_name = None - else: - file_name = file_desc.name - - if scope is None: - scope = {} - - nested = [ - self._ConvertMessageDescriptor( - nested, desc_name, file_desc, scope, syntax) - for nested in desc_proto.nested_type] - enums = [ - self._ConvertEnumDescriptor(enum, desc_name, file_desc, None, - scope, False) - for enum in desc_proto.enum_type] - fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc) - for index, field in enumerate(desc_proto.field)] - extensions = [ - self._MakeFieldDescriptor(extension, desc_name, index, file_desc, - is_extension=True) - for index, extension in enumerate(desc_proto.extension)] - oneofs = [ - # pylint: disable=g-complex-comprehension - descriptor.OneofDescriptor( - desc.name, - '.'.join((desc_name, desc.name)), - index, - None, - [], - _OptionsOrNone(desc), - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - for index, desc in enumerate(desc_proto.oneof_decl) - ] - extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range] - if extension_ranges: - is_extendable = True - else: - is_extendable = False - desc = descriptor.Descriptor( - name=desc_proto.name, - full_name=desc_name, - filename=file_name, - containing_type=None, - fields=fields, - oneofs=oneofs, - nested_types=nested, - enum_types=enums, - extensions=extensions, - options=_OptionsOrNone(desc_proto), - is_extendable=is_extendable, - extension_ranges=extension_ranges, - file=file_desc, - serialized_start=None, - serialized_end=None, - syntax=syntax, - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - for nested in desc.nested_types: - nested.containing_type = desc - for enum in desc.enum_types: - enum.containing_type = desc - for field_index, field_desc in enumerate(desc_proto.field): - if field_desc.HasField('oneof_index'): - oneof_index = field_desc.oneof_index - oneofs[oneof_index].fields.append(fields[field_index]) - fields[field_index].containing_oneof = oneofs[oneof_index] - - scope[_PrefixWithDot(desc_name)] = desc - self._CheckConflictRegister(desc, desc.full_name, desc.file.name) - self._descriptors[desc_name] = desc - return desc - - def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None, - containing_type=None, scope=None, top_level=False): - """Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf. - - Args: - enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message. - package: Optional package name for the new message EnumDescriptor. - file_desc: The file containing the enum descriptor. - containing_type: The type containing this enum. - scope: Scope containing available types. - top_level: If True, the enum is a top level symbol. If False, the enum - is defined inside a message. - - Returns: - The added descriptor - """ - - if package: - enum_name = '.'.join((package, enum_proto.name)) - else: - enum_name = enum_proto.name - - if file_desc is None: - file_name = None - else: - file_name = file_desc.name - - values = [self._MakeEnumValueDescriptor(value, index) - for index, value in enumerate(enum_proto.value)] - desc = descriptor.EnumDescriptor(name=enum_proto.name, - full_name=enum_name, - filename=file_name, - file=file_desc, - values=values, - containing_type=containing_type, - options=_OptionsOrNone(enum_proto), - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - scope['.%s' % enum_name] = desc - self._CheckConflictRegister(desc, desc.full_name, desc.file.name) - self._enum_descriptors[enum_name] = desc - - # Add top level enum values. - if top_level: - for value in values: - full_name = _NormalizeFullyQualifiedName( - '.'.join((package, value.name))) - self._CheckConflictRegister(value, full_name, file_name) - self._top_enum_values[full_name] = value - - return desc - - def _MakeFieldDescriptor(self, field_proto, message_name, index, - file_desc, is_extension=False): - """Creates a field descriptor from a FieldDescriptorProto. - - For message and enum type fields, this method will do a look up - in the pool for the appropriate descriptor for that type. If it - is unavailable, it will fall back to the _source function to - create it. If this type is still unavailable, construction will - fail. - - Args: - field_proto: The proto describing the field. - message_name: The name of the containing message. - index: Index of the field - file_desc: The file containing the field descriptor. - is_extension: Indication that this field is for an extension. - - Returns: - An initialized FieldDescriptor object - """ - - if message_name: - full_name = '.'.join((message_name, field_proto.name)) - else: - full_name = field_proto.name - - if field_proto.json_name: - json_name = field_proto.json_name - else: - json_name = None - - return descriptor.FieldDescriptor( - name=field_proto.name, - full_name=full_name, - index=index, - number=field_proto.number, - type=field_proto.type, - cpp_type=None, - message_type=None, - enum_type=None, - containing_type=None, - label=field_proto.label, - has_default_value=False, - default_value=None, - is_extension=is_extension, - extension_scope=None, - options=_OptionsOrNone(field_proto), - json_name=json_name, - file=file_desc, - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - - def _SetAllFieldTypes(self, package, desc_proto, scope): - """Sets all the descriptor's fields's types. - - This method also sets the containing types on any extensions. - - Args: - package: The current package of desc_proto. - desc_proto: The message descriptor to update. - scope: Enclosing scope of available types. - """ - - package = _PrefixWithDot(package) - - main_desc = self._GetTypeFromScope(package, desc_proto.name, scope) - - if package == '.': - nested_package = _PrefixWithDot(desc_proto.name) - else: - nested_package = '.'.join([package, desc_proto.name]) - - for field_proto, field_desc in zip(desc_proto.field, main_desc.fields): - self._SetFieldType(field_proto, field_desc, nested_package, scope) - - for extension_proto, extension_desc in ( - zip(desc_proto.extension, main_desc.extensions)): - extension_desc.containing_type = self._GetTypeFromScope( - nested_package, extension_proto.extendee, scope) - self._SetFieldType(extension_proto, extension_desc, nested_package, scope) - - for nested_type in desc_proto.nested_type: - self._SetAllFieldTypes(nested_package, nested_type, scope) - - def _SetFieldType(self, field_proto, field_desc, package, scope): - """Sets the field's type, cpp_type, message_type and enum_type. - - Args: - field_proto: Data about the field in proto format. - field_desc: The descriptor to modify. - package: The package the field's container is in. - scope: Enclosing scope of available types. - """ - if field_proto.type_name: - desc = self._GetTypeFromScope(package, field_proto.type_name, scope) - else: - desc = None - - if not field_proto.HasField('type'): - if isinstance(desc, descriptor.Descriptor): - field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE - else: - field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM - - field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType( - field_proto.type) - - if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE - or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP): - field_desc.message_type = desc - - if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: - field_desc.enum_type = desc - - if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED: - field_desc.has_default_value = False - field_desc.default_value = [] - elif field_proto.HasField('default_value'): - field_desc.has_default_value = True - if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or - field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): - field_desc.default_value = float(field_proto.default_value) - elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: - field_desc.default_value = field_proto.default_value - elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: - field_desc.default_value = field_proto.default_value.lower() == 'true' - elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: - field_desc.default_value = field_desc.enum_type.values_by_name[ - field_proto.default_value].number - elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: - field_desc.default_value = text_encoding.CUnescape( - field_proto.default_value) - elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: - field_desc.default_value = None - else: - # All other types are of the "int" type. - field_desc.default_value = int(field_proto.default_value) - else: - field_desc.has_default_value = False - if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or - field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): - field_desc.default_value = 0.0 - elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: - field_desc.default_value = u'' - elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: - field_desc.default_value = False - elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: - field_desc.default_value = field_desc.enum_type.values[0].number - elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: - field_desc.default_value = b'' - elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: - field_desc.default_value = None - elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP: - field_desc.default_value = None - else: - # All other types are of the "int" type. - field_desc.default_value = 0 - - field_desc.type = field_proto.type - - def _MakeEnumValueDescriptor(self, value_proto, index): - """Creates a enum value descriptor object from a enum value proto. - - Args: - value_proto: The proto describing the enum value. - index: The index of the enum value. - - Returns: - An initialized EnumValueDescriptor object. - """ - - return descriptor.EnumValueDescriptor( - name=value_proto.name, - index=index, - number=value_proto.number, - options=_OptionsOrNone(value_proto), - type=None, - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - - def _MakeServiceDescriptor(self, service_proto, service_index, scope, - package, file_desc): - """Make a protobuf ServiceDescriptor given a ServiceDescriptorProto. - - Args: - service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message. - service_index: The index of the service in the File. - scope: Dict mapping short and full symbols to message and enum types. - package: Optional package name for the new message EnumDescriptor. - file_desc: The file containing the service descriptor. - - Returns: - The added descriptor. - """ - - if package: - service_name = '.'.join((package, service_proto.name)) - else: - service_name = service_proto.name - - methods = [self._MakeMethodDescriptor(method_proto, service_name, package, - scope, index) - for index, method_proto in enumerate(service_proto.method)] - desc = descriptor.ServiceDescriptor( - name=service_proto.name, - full_name=service_name, - index=service_index, - methods=methods, - options=_OptionsOrNone(service_proto), - file=file_desc, - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - self._CheckConflictRegister(desc, desc.full_name, desc.file.name) - self._service_descriptors[service_name] = desc - return desc - - def _MakeMethodDescriptor(self, method_proto, service_name, package, scope, - index): - """Creates a method descriptor from a MethodDescriptorProto. - - Args: - method_proto: The proto describing the method. - service_name: The name of the containing service. - package: Optional package name to look up for types. - scope: Scope containing available types. - index: Index of the method in the service. - - Returns: - An initialized MethodDescriptor object. - """ - full_name = '.'.join((service_name, method_proto.name)) - input_type = self._GetTypeFromScope( - package, method_proto.input_type, scope) - output_type = self._GetTypeFromScope( - package, method_proto.output_type, scope) - return descriptor.MethodDescriptor( - name=method_proto.name, - full_name=full_name, - index=index, - containing_service=None, - input_type=input_type, - output_type=output_type, - client_streaming=method_proto.client_streaming, - server_streaming=method_proto.server_streaming, - options=_OptionsOrNone(method_proto), - # pylint: disable=protected-access - create_key=descriptor._internal_create_key) - - def _ExtractSymbols(self, descriptors): - """Pulls out all the symbols from descriptor protos. - - Args: - descriptors: The messages to extract descriptors from. - Yields: - A two element tuple of the type name and descriptor object. - """ - - for desc in descriptors: - yield (_PrefixWithDot(desc.full_name), desc) - for symbol in self._ExtractSymbols(desc.nested_types): - yield symbol - for enum in desc.enum_types: - yield (_PrefixWithDot(enum.full_name), enum) - - def _GetDeps(self, dependencies, visited=None): - """Recursively finds dependencies for file protos. - - Args: - dependencies: The names of the files being depended on. - visited: The names of files already found. - - Yields: - Each direct and indirect dependency. - """ - - visited = visited or set() - for dependency in dependencies: - if dependency not in visited: - visited.add(dependency) - dep_desc = self.FindFileByName(dependency) - yield dep_desc - public_files = [d.name for d in dep_desc.public_dependencies] - yield from self._GetDeps(public_files, visited) - - def _GetTypeFromScope(self, package, type_name, scope): - """Finds a given type name in the current scope. - - Args: - package: The package the proto should be located in. - type_name: The name of the type to be found in the scope. - scope: Dict mapping short and full symbols to message and enum types. - - Returns: - The descriptor for the requested type. - """ - if type_name not in scope: - components = _PrefixWithDot(package).split('.') - while components: - possible_match = '.'.join(components + [type_name]) - if possible_match in scope: - type_name = possible_match - break - else: - components.pop(-1) - return scope[type_name] - - -def _PrefixWithDot(name): - return name if name.startswith('.') else '.%s' % name - - -if _USE_C_DESCRIPTORS: - # TODO(amauryfa): This pool could be constructed from Python code, when we - # support a flag like 'use_cpp_generated_pool=True'. - # pylint: disable=protected-access - _DEFAULT = descriptor._message.default_pool -else: - _DEFAULT = DescriptorPool() - - -def Default(): - return _DEFAULT diff --git a/script.module.protobuf/resources/lib/google/protobuf/duration_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/duration_pb2.py deleted file mode 100644 index 34712622d..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/duration_pb2.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/duration.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/duration.proto\x12\x0fgoogle.protobuf\":\n\x08\x44uration\x12\x18\n\x07seconds\x18\x01 \x01(\x03R\x07seconds\x12\x14\n\x05nanos\x18\x02 \x01(\x05R\x05nanosB\x83\x01\n\x13\x63om.google.protobufB\rDurationProtoP\x01Z1google.golang.org/protobuf/types/known/durationpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rDurationProtoP\001Z1google.golang.org/protobuf/types/known/durationpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_DURATION']._serialized_start=51 - _globals['_DURATION']._serialized_end=109 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/empty_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/empty_pb2.py deleted file mode 100644 index cbecdfe26..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/empty_pb2.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/empty.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bgoogle/protobuf/empty.proto\x12\x0fgoogle.protobuf\"\x07\n\x05\x45mptyB}\n\x13\x63om.google.protobufB\nEmptyProtoP\x01Z.google.golang.org/protobuf/types/known/emptypb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\nEmptyProtoP\001Z.google.golang.org/protobuf/types/known/emptypb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_EMPTY']._serialized_start=48 - _globals['_EMPTY']._serialized_end=55 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/field_mask_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/field_mask_pb2.py deleted file mode 100644 index f5c1e45d4..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/field_mask_pb2.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/field_mask.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/field_mask.proto\x12\x0fgoogle.protobuf\"!\n\tFieldMask\x12\x14\n\x05paths\x18\x01 \x03(\tR\x05pathsB\x85\x01\n\x13\x63om.google.protobufB\x0e\x46ieldMaskProtoP\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016FieldMaskProtoP\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_FIELDMASK']._serialized_start=53 - _globals['_FIELDMASK']._serialized_end=86 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/__init__.py b/script.module.protobuf/resources/lib/google/protobuf/internal/__init__.py deleted file mode 100644 index 7d2e571a1..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/_parameterized.py b/script.module.protobuf/resources/lib/google/protobuf/internal/_parameterized.py deleted file mode 100644 index 2f4a3b6b7..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/_parameterized.py +++ /dev/null @@ -1,443 +0,0 @@ -#! /usr/bin/env python -# -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Adds support for parameterized tests to Python's unittest TestCase class. - -A parameterized test is a method in a test case that is invoked with different -argument tuples. - -A simple example: - - class AdditionExample(_parameterized.TestCase): - @_parameterized.parameters( - (1, 2, 3), - (4, 5, 9), - (1, 1, 3)) - def testAddition(self, op1, op2, result): - self.assertEqual(result, op1 + op2) - - -Each invocation is a separate test case and properly isolated just -like a normal test method, with its own setUp/tearDown cycle. In the -example above, there are three separate testcases, one of which will -fail due to an assertion error (1 + 1 != 3). - -Parameters for individual test cases can be tuples (with positional parameters) -or dictionaries (with named parameters): - - class AdditionExample(_parameterized.TestCase): - @_parameterized.parameters( - {'op1': 1, 'op2': 2, 'result': 3}, - {'op1': 4, 'op2': 5, 'result': 9}, - ) - def testAddition(self, op1, op2, result): - self.assertEqual(result, op1 + op2) - -If a parameterized test fails, the error message will show the -original test name (which is modified internally) and the arguments -for the specific invocation, which are part of the string returned by -the shortDescription() method on test cases. - -The id method of the test, used internally by the unittest framework, -is also modified to show the arguments. To make sure that test names -stay the same across several invocations, object representations like - - >>> class Foo(object): - ... pass - >>> repr(Foo()) - '<__main__.Foo object at 0x23d8610>' - -are turned into '<__main__.Foo>'. For even more descriptive names, -especially in test logs, you can use the named_parameters decorator. In -this case, only tuples are supported, and the first parameters has to -be a string (or an object that returns an apt name when converted via -str()): - - class NamedExample(_parameterized.TestCase): - @_parameterized.named_parameters( - ('Normal', 'aa', 'aaa', True), - ('EmptyPrefix', '', 'abc', True), - ('BothEmpty', '', '', True)) - def testStartsWith(self, prefix, string, result): - self.assertEqual(result, strings.startswith(prefix)) - -Named tests also have the benefit that they can be run individually -from the command line: - - $ testmodule.py NamedExample.testStartsWithNormal - . - -------------------------------------------------------------------- - Ran 1 test in 0.000s - - OK - -Parameterized Classes -===================== -If invocation arguments are shared across test methods in a single -TestCase class, instead of decorating all test methods -individually, the class itself can be decorated: - - @_parameterized.parameters( - (1, 2, 3) - (4, 5, 9)) - class ArithmeticTest(_parameterized.TestCase): - def testAdd(self, arg1, arg2, result): - self.assertEqual(arg1 + arg2, result) - - def testSubtract(self, arg2, arg2, result): - self.assertEqual(result - arg1, arg2) - -Inputs from Iterables -===================== -If parameters should be shared across several test cases, or are dynamically -created from other sources, a single non-tuple iterable can be passed into -the decorator. This iterable will be used to obtain the test cases: - - class AdditionExample(_parameterized.TestCase): - @_parameterized.parameters( - c.op1, c.op2, c.result for c in testcases - ) - def testAddition(self, op1, op2, result): - self.assertEqual(result, op1 + op2) - - -Single-Argument Test Methods -============================ -If a test method takes only one argument, the single argument does not need to -be wrapped into a tuple: - - class NegativeNumberExample(_parameterized.TestCase): - @_parameterized.parameters( - -1, -3, -4, -5 - ) - def testIsNegative(self, arg): - self.assertTrue(IsNegative(arg)) -""" - -__author__ = 'tmarek@google.com (Torsten Marek)' - -import functools -import re -import types -import unittest -import uuid - -try: - # Since python 3 - import collections.abc as collections_abc -except ImportError: - # Won't work after python 3.8 - import collections as collections_abc - -ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') -_SEPARATOR = uuid.uuid1().hex -_FIRST_ARG = object() -_ARGUMENT_REPR = object() - - -def _CleanRepr(obj): - return ADDR_RE.sub(r'<\1>', repr(obj)) - - -# Helper function formerly from the unittest module, removed from it in -# Python 2.7. -def _StrClass(cls): - return '%s.%s' % (cls.__module__, cls.__name__) - - -def _NonStringIterable(obj): - return (isinstance(obj, collections_abc.Iterable) and - not isinstance(obj, str)) - - -def _FormatParameterList(testcase_params): - if isinstance(testcase_params, collections_abc.Mapping): - return ', '.join('%s=%s' % (argname, _CleanRepr(value)) - for argname, value in testcase_params.items()) - elif _NonStringIterable(testcase_params): - return ', '.join(map(_CleanRepr, testcase_params)) - else: - return _FormatParameterList((testcase_params,)) - - -class _ParameterizedTestIter(object): - """Callable and iterable class for producing new test cases.""" - - def __init__(self, test_method, testcases, naming_type): - """Returns concrete test functions for a test and a list of parameters. - - The naming_type is used to determine the name of the concrete - functions as reported by the unittest framework. If naming_type is - _FIRST_ARG, the testcases must be tuples, and the first element must - have a string representation that is a valid Python identifier. - - Args: - test_method: The decorated test method. - testcases: (list of tuple/dict) A list of parameter - tuples/dicts for individual test invocations. - naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. - """ - self._test_method = test_method - self.testcases = testcases - self._naming_type = naming_type - - def __call__(self, *args, **kwargs): - raise RuntimeError('You appear to be running a parameterized test case ' - 'without having inherited from parameterized.' - 'TestCase. This is bad because none of ' - 'your test cases are actually being run.') - - def __iter__(self): - test_method = self._test_method - naming_type = self._naming_type - - def MakeBoundParamTest(testcase_params): - @functools.wraps(test_method) - def BoundParamTest(self): - if isinstance(testcase_params, collections_abc.Mapping): - test_method(self, **testcase_params) - elif _NonStringIterable(testcase_params): - test_method(self, *testcase_params) - else: - test_method(self, testcase_params) - - if naming_type is _FIRST_ARG: - # Signal the metaclass that the name of the test function is unique - # and descriptive. - BoundParamTest.__x_use_name__ = True - BoundParamTest.__name__ += str(testcase_params[0]) - testcase_params = testcase_params[1:] - elif naming_type is _ARGUMENT_REPR: - # __x_extra_id__ is used to pass naming information to the __new__ - # method of TestGeneratorMetaclass. - # The metaclass will make sure to create a unique, but nondescriptive - # name for this test. - BoundParamTest.__x_extra_id__ = '(%s)' % ( - _FormatParameterList(testcase_params),) - else: - raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) - - BoundParamTest.__doc__ = '%s(%s)' % ( - BoundParamTest.__name__, _FormatParameterList(testcase_params)) - if test_method.__doc__: - BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) - return BoundParamTest - return (MakeBoundParamTest(c) for c in self.testcases) - - -def _IsSingletonList(testcases): - """True iff testcases contains only a single non-tuple element.""" - return len(testcases) == 1 and not isinstance(testcases[0], tuple) - - -def _ModifyClass(class_object, testcases, naming_type): - assert not getattr(class_object, '_id_suffix', None), ( - 'Cannot add parameters to %s,' - ' which already has parameterized methods.' % (class_object,)) - class_object._id_suffix = id_suffix = {} - # We change the size of __dict__ while we iterate over it, - # which Python 3.x will complain about, so use copy(). - for name, obj in class_object.__dict__.copy().items(): - if (name.startswith(unittest.TestLoader.testMethodPrefix) - and isinstance(obj, types.FunctionType)): - delattr(class_object, name) - methods = {} - _UpdateClassDictForParamTestCase( - methods, id_suffix, name, - _ParameterizedTestIter(obj, testcases, naming_type)) - for name, meth in methods.items(): - setattr(class_object, name, meth) - - -def _ParameterDecorator(naming_type, testcases): - """Implementation of the parameterization decorators. - - Args: - naming_type: The naming type. - testcases: Testcase parameters. - - Returns: - A function for modifying the decorated object. - """ - def _Apply(obj): - if isinstance(obj, type): - _ModifyClass( - obj, - list(testcases) if not isinstance(testcases, collections_abc.Sequence) - else testcases, - naming_type) - return obj - else: - return _ParameterizedTestIter(obj, testcases, naming_type) - - if _IsSingletonList(testcases): - assert _NonStringIterable(testcases[0]), ( - 'Single parameter argument must be a non-string iterable') - testcases = testcases[0] - - return _Apply - - -def parameters(*testcases): # pylint: disable=invalid-name - """A decorator for creating parameterized tests. - - See the module docstring for a usage example. - Args: - *testcases: Parameters for the decorated method, either a single - iterable, or a list of tuples/dicts/objects (for tests - with only one argument). - - Returns: - A test generator to be handled by TestGeneratorMetaclass. - """ - return _ParameterDecorator(_ARGUMENT_REPR, testcases) - - -def named_parameters(*testcases): # pylint: disable=invalid-name - """A decorator for creating parameterized tests. - - See the module docstring for a usage example. The first element of - each parameter tuple should be a string and will be appended to the - name of the test method. - - Args: - *testcases: Parameters for the decorated method, either a single - iterable, or a list of tuples. - - Returns: - A test generator to be handled by TestGeneratorMetaclass. - """ - return _ParameterDecorator(_FIRST_ARG, testcases) - - -class TestGeneratorMetaclass(type): - """Metaclass for test cases with test generators. - - A test generator is an iterable in a testcase that produces callables. These - callables must be single-argument methods. These methods are injected into - the class namespace and the original iterable is removed. If the name of the - iterable conforms to the test pattern, the injected methods will be picked - up as tests by the unittest framework. - - In general, it is supposed to be used in conjunction with the - parameters decorator. - """ - - def __new__(mcs, class_name, bases, dct): - dct['_id_suffix'] = id_suffix = {} - for name, obj in dct.copy().items(): - if (name.startswith(unittest.TestLoader.testMethodPrefix) and - _NonStringIterable(obj)): - iterator = iter(obj) - dct.pop(name) - _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) - - return type.__new__(mcs, class_name, bases, dct) - - -def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): - """Adds individual test cases to a dictionary. - - Args: - dct: The target dictionary. - id_suffix: The dictionary for mapping names to test IDs. - name: The original name of the test case. - iterator: The iterator generating the individual test cases. - """ - for idx, func in enumerate(iterator): - assert callable(func), 'Test generators must yield callables, got %r' % ( - func,) - if getattr(func, '__x_use_name__', False): - new_name = func.__name__ - else: - new_name = '%s%s%d' % (name, _SEPARATOR, idx) - assert new_name not in dct, ( - 'Name of parameterized test case "%s" not unique' % (new_name,)) - dct[new_name] = func - id_suffix[new_name] = getattr(func, '__x_extra_id__', '') - - -class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass): - """Base class for test cases using the parameters decorator.""" - - def _OriginalName(self): - return self._testMethodName.split(_SEPARATOR)[0] - - def __str__(self): - return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) - - def id(self): # pylint: disable=invalid-name - """Returns the descriptive ID of the test. - - This is used internally by the unittesting framework to get a name - for the test to be used in reports. - - Returns: - The test id. - """ - return '%s.%s%s' % (_StrClass(self.__class__), - self._OriginalName(), - self._id_suffix.get(self._testMethodName, '')) - - -def CoopTestCase(other_base_class): - """Returns a new base class with a cooperative metaclass base. - - This enables the TestCase to be used in combination - with other base classes that have custom metaclasses, such as - mox.MoxTestBase. - - Only works with metaclasses that do not override type.__new__. - - Example: - - import google3 - import mox - - from google.protobuf.internal import _parameterized - - class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)): - ... - - Args: - other_base_class: (class) A test case base class. - - Returns: - A new class object. - """ - metaclass = type( - 'CoopMetaclass', - (other_base_class.__metaclass__, - TestGeneratorMetaclass), {}) - return metaclass( - 'CoopTestCase', - (other_base_class, TestCase), {}) diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/api_implementation.py b/script.module.protobuf/resources/lib/google/protobuf/internal/api_implementation.py deleted file mode 100644 index 7d20bd221..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/api_implementation.py +++ /dev/null @@ -1,163 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Determine which implementation of the protobuf API is used in this process. -""" - -import importlib -import os -import sys -import warnings - - -def _ApiVersionToImplementationType(api_version): - if api_version == 2: - return 'cpp' - if api_version == 1: - raise ValueError('api_version=1 is no longer supported.') - if api_version == 0: - return 'python' - return None - - -_implementation_type = None -try: - # pylint: disable=g-import-not-at-top - from google.protobuf.internal import _api_implementation - # The compile-time constants in the _api_implementation module can be used to - # switch to a certain implementation of the Python API at build time. - _implementation_type = _ApiVersionToImplementationType( - _api_implementation.api_version) -except ImportError: - pass # Unspecified by compiler flags. - - -def _CanImport(mod_name): - try: - mod = importlib.import_module(mod_name) - # Work around a known issue in the classic bootstrap .par import hook. - if not mod: - raise ImportError(mod_name + ' import succeeded but was None') - return True - except ImportError: - return False - - -if _implementation_type is None: - if _CanImport('google._upb._message'): - _implementation_type = 'upb' - elif _CanImport('google.protobuf.pyext._message'): - _implementation_type = 'cpp' - else: - _implementation_type = 'python' - - -# This environment variable can be used to switch to a certain implementation -# of the Python API, overriding the compile-time constants in the -# _api_implementation module. Right now only 'python', 'cpp' and 'upb' are -# valid values. Any other value will raise error. -_implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', - _implementation_type) - -if _implementation_type not in ('python', 'cpp', 'upb'): - raise ValueError('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION {0} is not ' - 'supported. Please set to \'python\', \'cpp\' or ' - '\'upb\'.'.format(_implementation_type)) - -if 'PyPy' in sys.version and _implementation_type == 'cpp': - warnings.warn('PyPy does not work yet with cpp protocol buffers. ' - 'Falling back to the python implementation.') - _implementation_type = 'python' - -_c_module = None - -if _implementation_type == 'cpp': - try: - # pylint: disable=g-import-not-at-top - from google.protobuf.pyext import _message - sys.modules['google3.net.proto2.python.internal.cpp._message'] = _message - _c_module = _message - del _message - except ImportError: - # TODO(jieluo): fail back to python - warnings.warn( - 'Selected implementation cpp is not available.') - pass - -if _implementation_type == 'upb': - try: - # pylint: disable=g-import-not-at-top - from google._upb import _message - _c_module = _message - del _message - except ImportError: - warnings.warn('Selected implementation upb is not available. ' - 'Falling back to the python implementation.') - _implementation_type = 'python' - pass - -# Detect if serialization should be deterministic by default -try: - # The presence of this module in a build allows the proto implementation to - # be upgraded merely via build deps. - # - # NOTE: Merely importing this automatically enables deterministic proto - # serialization for C++ code, but we still need to export it as a boolean so - # that we can do the same for `_implementation_type == 'python'`. - # - # NOTE2: It is possible for C++ code to enable deterministic serialization by - # default _without_ affecting Python code, if the C++ implementation is not in - # use by this module. That is intended behavior, so we don't actually expose - # this boolean outside of this module. - # - # pylint: disable=g-import-not-at-top,unused-import - from google.protobuf import enable_deterministic_proto_serialization - _python_deterministic_proto_serialization = True -except ImportError: - _python_deterministic_proto_serialization = False - - -# Usage of this function is discouraged. Clients shouldn't care which -# implementation of the API is in use. Note that there is no guarantee -# that differences between APIs will be maintained. -# Please don't use this function if possible. -def Type(): - return _implementation_type - - -# See comment on 'Type' above. -# TODO(jieluo): Remove the API, it returns a constant. b/228102101 -def Version(): - return 2 - - -# For internal use only -def IsPythonDefaultSerializationDeterministic(): - return _python_deterministic_proto_serialization diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/builder.py b/script.module.protobuf/resources/lib/google/protobuf/internal/builder.py deleted file mode 100644 index 64353ee4a..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/builder.py +++ /dev/null @@ -1,130 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Builds descriptors, message classes and services for generated _pb2.py. - -This file is only called in python generated _pb2.py files. It builds -descriptors, message classes and services that users can directly use -in generated code. -""" - -__author__ = 'jieluo@google.com (Jie Luo)' - -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database - -_sym_db = _symbol_database.Default() - - -def BuildMessageAndEnumDescriptors(file_des, module): - """Builds message and enum descriptors. - - Args: - file_des: FileDescriptor of the .proto file - module: Generated _pb2 module - """ - - def BuildNestedDescriptors(msg_des, prefix): - for (name, nested_msg) in msg_des.nested_types_by_name.items(): - module_name = prefix + name.upper() - module[module_name] = nested_msg - BuildNestedDescriptors(nested_msg, module_name + '_') - for enum_des in msg_des.enum_types: - module[prefix + enum_des.name.upper()] = enum_des - - for (name, msg_des) in file_des.message_types_by_name.items(): - module_name = '_' + name.upper() - module[module_name] = msg_des - BuildNestedDescriptors(msg_des, module_name + '_') - - -def BuildTopDescriptorsAndMessages(file_des, module_name, module): - """Builds top level descriptors and message classes. - - Args: - file_des: FileDescriptor of the .proto file - module_name: str, the name of generated _pb2 module - module: Generated _pb2 module - """ - - def BuildMessage(msg_des): - create_dict = {} - for (name, nested_msg) in msg_des.nested_types_by_name.items(): - create_dict[name] = BuildMessage(nested_msg) - create_dict['DESCRIPTOR'] = msg_des - create_dict['__module__'] = module_name - message_class = _reflection.GeneratedProtocolMessageType( - msg_des.name, (_message.Message,), create_dict) - _sym_db.RegisterMessage(message_class) - return message_class - - # top level enums - for (name, enum_des) in file_des.enum_types_by_name.items(): - module['_' + name.upper()] = enum_des - module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des) - for enum_value in enum_des.values: - module[enum_value.name] = enum_value.number - - # top level extensions - for (name, extension_des) in file_des.extensions_by_name.items(): - module[name.upper() + '_FIELD_NUMBER'] = extension_des.number - module[name] = extension_des - - # services - for (name, service) in file_des.services_by_name.items(): - module['_' + name.upper()] = service - - # Build messages. - for (name, msg_des) in file_des.message_types_by_name.items(): - module[name] = BuildMessage(msg_des) - - -def BuildServices(file_des, module_name, module): - """Builds services classes and services stub class. - - Args: - file_des: FileDescriptor of the .proto file - module_name: str, the name of generated _pb2 module - module: Generated _pb2 module - """ - # pylint: disable=g-import-not-at-top - from google.protobuf import service as _service - from google.protobuf import service_reflection - # pylint: enable=g-import-not-at-top - for (name, service) in file_des.services_by_name.items(): - module[name] = service_reflection.GeneratedServiceType( - name, (_service.Service,), - dict(DESCRIPTOR=service, __module__=module_name)) - stub_name = name + '_Stub' - module[stub_name] = service_reflection.GeneratedServiceStubType( - stub_name, (module[name],), - dict(DESCRIPTOR=service, __module__=module_name)) diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/containers.py b/script.module.protobuf/resources/lib/google/protobuf/internal/containers.py deleted file mode 100644 index 29fbb53d2..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/containers.py +++ /dev/null @@ -1,710 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains container classes to represent different protocol buffer types. - -This file defines container classes which represent categories of protocol -buffer field types which need extra maintenance. Currently these categories -are: - -- Repeated scalar fields - These are all repeated fields which aren't - composite (e.g. they are of simple types like int32, string, etc). -- Repeated composite fields - Repeated fields which are composite. This - includes groups and nested messages. -""" - -import collections.abc -import copy -import pickle -from typing import ( - Any, - Iterable, - Iterator, - List, - MutableMapping, - MutableSequence, - NoReturn, - Optional, - Sequence, - TypeVar, - Union, - overload, -) - - -_T = TypeVar('_T') -_K = TypeVar('_K') -_V = TypeVar('_V') - - -class BaseContainer(Sequence[_T]): - """Base container class.""" - - # Minimizes memory usage and disallows assignment to other attributes. - __slots__ = ['_message_listener', '_values'] - - def __init__(self, message_listener: Any) -> None: - """ - Args: - message_listener: A MessageListener implementation. - The RepeatedScalarFieldContainer will call this object's - Modified() method when it is modified. - """ - self._message_listener = message_listener - self._values = [] - - @overload - def __getitem__(self, key: int) -> _T: - ... - - @overload - def __getitem__(self, key: slice) -> List[_T]: - ... - - def __getitem__(self, key): - """Retrieves item by the specified key.""" - return self._values[key] - - def __len__(self) -> int: - """Returns the number of elements in the container.""" - return len(self._values) - - def __ne__(self, other: Any) -> bool: - """Checks if another instance isn't equal to this one.""" - # The concrete classes should define __eq__. - return not self == other - - __hash__ = None - - def __repr__(self) -> str: - return repr(self._values) - - def sort(self, *args, **kwargs) -> None: - # Continue to support the old sort_function keyword argument. - # This is expected to be a rare occurrence, so use LBYL to avoid - # the overhead of actually catching KeyError. - if 'sort_function' in kwargs: - kwargs['cmp'] = kwargs.pop('sort_function') - self._values.sort(*args, **kwargs) - - def reverse(self) -> None: - self._values.reverse() - - -# TODO(slebedev): Remove this. BaseContainer does *not* conform to -# MutableSequence, only its subclasses do. -collections.abc.MutableSequence.register(BaseContainer) - - -class RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]): - """Simple, type-checked, list-like container for holding repeated scalars.""" - - # Disallows assignment to other attributes. - __slots__ = ['_type_checker'] - - def __init__( - self, - message_listener: Any, - type_checker: Any, - ) -> None: - """Args: - - message_listener: A MessageListener implementation. The - RepeatedScalarFieldContainer will call this object's Modified() method - when it is modified. - type_checker: A type_checkers.ValueChecker instance to run on elements - inserted into this container. - """ - super().__init__(message_listener) - self._type_checker = type_checker - - def append(self, value: _T) -> None: - """Appends an item to the list. Similar to list.append().""" - self._values.append(self._type_checker.CheckValue(value)) - if not self._message_listener.dirty: - self._message_listener.Modified() - - def insert(self, key: int, value: _T) -> None: - """Inserts the item at the specified position. Similar to list.insert().""" - self._values.insert(key, self._type_checker.CheckValue(value)) - if not self._message_listener.dirty: - self._message_listener.Modified() - - def extend(self, elem_seq: Iterable[_T]) -> None: - """Extends by appending the given iterable. Similar to list.extend().""" - if elem_seq is None: - return - try: - elem_seq_iter = iter(elem_seq) - except TypeError: - if not elem_seq: - # silently ignore falsy inputs :-/. - # TODO(ptucker): Deprecate this behavior. b/18413862 - return - raise - - new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter] - if new_values: - self._values.extend(new_values) - self._message_listener.Modified() - - def MergeFrom( - self, - other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]], - ) -> None: - """Appends the contents of another repeated field of the same type to this - one. We do not check the types of the individual fields. - """ - self._values.extend(other) - self._message_listener.Modified() - - def remove(self, elem: _T): - """Removes an item from the list. Similar to list.remove().""" - self._values.remove(elem) - self._message_listener.Modified() - - def pop(self, key: Optional[int] = -1) -> _T: - """Removes and returns an item at a given index. Similar to list.pop().""" - value = self._values[key] - self.__delitem__(key) - return value - - @overload - def __setitem__(self, key: int, value: _T) -> None: - ... - - @overload - def __setitem__(self, key: slice, value: Iterable[_T]) -> None: - ... - - def __setitem__(self, key, value) -> None: - """Sets the item on the specified position.""" - if isinstance(key, slice): - if key.step is not None: - raise ValueError('Extended slices not supported') - self._values[key] = map(self._type_checker.CheckValue, value) - self._message_listener.Modified() - else: - self._values[key] = self._type_checker.CheckValue(value) - self._message_listener.Modified() - - def __delitem__(self, key: Union[int, slice]) -> None: - """Deletes the item at the specified position.""" - del self._values[key] - self._message_listener.Modified() - - def __eq__(self, other: Any) -> bool: - """Compares the current instance with another one.""" - if self is other: - return True - # Special case for the same type which should be common and fast. - if isinstance(other, self.__class__): - return other._values == self._values - # We are presumably comparing against some other sequence type. - return other == self._values - - def __deepcopy__( - self, - unused_memo: Any = None, - ) -> 'RepeatedScalarFieldContainer[_T]': - clone = RepeatedScalarFieldContainer( - copy.deepcopy(self._message_listener), self._type_checker) - clone.MergeFrom(self) - return clone - - def __reduce__(self, **kwargs) -> NoReturn: - raise pickle.PickleError( - "Can't pickle repeated scalar fields, convert to list first") - - -# TODO(slebedev): Constrain T to be a subtype of Message. -class RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]): - """Simple, list-like container for holding repeated composite fields.""" - - # Disallows assignment to other attributes. - __slots__ = ['_message_descriptor'] - - def __init__(self, message_listener: Any, message_descriptor: Any) -> None: - """ - Note that we pass in a descriptor instead of the generated directly, - since at the time we construct a _RepeatedCompositeFieldContainer we - haven't yet necessarily initialized the type that will be contained in the - container. - - Args: - message_listener: A MessageListener implementation. - The RepeatedCompositeFieldContainer will call this object's - Modified() method when it is modified. - message_descriptor: A Descriptor instance describing the protocol type - that should be present in this container. We'll use the - _concrete_class field of this descriptor when the client calls add(). - """ - super().__init__(message_listener) - self._message_descriptor = message_descriptor - - def add(self, **kwargs: Any) -> _T: - """Adds a new element at the end of the list and returns it. Keyword - arguments may be used to initialize the element. - """ - new_element = self._message_descriptor._concrete_class(**kwargs) - new_element._SetListener(self._message_listener) - self._values.append(new_element) - if not self._message_listener.dirty: - self._message_listener.Modified() - return new_element - - def append(self, value: _T) -> None: - """Appends one element by copying the message.""" - new_element = self._message_descriptor._concrete_class() - new_element._SetListener(self._message_listener) - new_element.CopyFrom(value) - self._values.append(new_element) - if not self._message_listener.dirty: - self._message_listener.Modified() - - def insert(self, key: int, value: _T) -> None: - """Inserts the item at the specified position by copying.""" - new_element = self._message_descriptor._concrete_class() - new_element._SetListener(self._message_listener) - new_element.CopyFrom(value) - self._values.insert(key, new_element) - if not self._message_listener.dirty: - self._message_listener.Modified() - - def extend(self, elem_seq: Iterable[_T]) -> None: - """Extends by appending the given sequence of elements of the same type - - as this one, copying each individual message. - """ - message_class = self._message_descriptor._concrete_class - listener = self._message_listener - values = self._values - for message in elem_seq: - new_element = message_class() - new_element._SetListener(listener) - new_element.MergeFrom(message) - values.append(new_element) - listener.Modified() - - def MergeFrom( - self, - other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]], - ) -> None: - """Appends the contents of another repeated field of the same type to this - one, copying each individual message. - """ - self.extend(other) - - def remove(self, elem: _T) -> None: - """Removes an item from the list. Similar to list.remove().""" - self._values.remove(elem) - self._message_listener.Modified() - - def pop(self, key: Optional[int] = -1) -> _T: - """Removes and returns an item at a given index. Similar to list.pop().""" - value = self._values[key] - self.__delitem__(key) - return value - - @overload - def __setitem__(self, key: int, value: _T) -> None: - ... - - @overload - def __setitem__(self, key: slice, value: Iterable[_T]) -> None: - ... - - def __setitem__(self, key, value): - # This method is implemented to make RepeatedCompositeFieldContainer - # structurally compatible with typing.MutableSequence. It is - # otherwise unsupported and will always raise an error. - raise TypeError( - f'{self.__class__.__name__} object does not support item assignment') - - def __delitem__(self, key: Union[int, slice]) -> None: - """Deletes the item at the specified position.""" - del self._values[key] - self._message_listener.Modified() - - def __eq__(self, other: Any) -> bool: - """Compares the current instance with another one.""" - if self is other: - return True - if not isinstance(other, self.__class__): - raise TypeError('Can only compare repeated composite fields against ' - 'other repeated composite fields.') - return self._values == other._values - - -class ScalarMap(MutableMapping[_K, _V]): - """Simple, type-checked, dict-like container for holding repeated scalars.""" - - # Disallows assignment to other attributes. - __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener', - '_entry_descriptor'] - - def __init__( - self, - message_listener: Any, - key_checker: Any, - value_checker: Any, - entry_descriptor: Any, - ) -> None: - """ - Args: - message_listener: A MessageListener implementation. - The ScalarMap will call this object's Modified() method when it - is modified. - key_checker: A type_checkers.ValueChecker instance to run on keys - inserted into this container. - value_checker: A type_checkers.ValueChecker instance to run on values - inserted into this container. - entry_descriptor: The MessageDescriptor of a map entry: key and value. - """ - self._message_listener = message_listener - self._key_checker = key_checker - self._value_checker = value_checker - self._entry_descriptor = entry_descriptor - self._values = {} - - def __getitem__(self, key: _K) -> _V: - try: - return self._values[key] - except KeyError: - key = self._key_checker.CheckValue(key) - val = self._value_checker.DefaultValue() - self._values[key] = val - return val - - def __contains__(self, item: _K) -> bool: - # We check the key's type to match the strong-typing flavor of the API. - # Also this makes it easier to match the behavior of the C++ implementation. - self._key_checker.CheckValue(item) - return item in self._values - - @overload - def get(self, key: _K) -> Optional[_V]: - ... - - @overload - def get(self, key: _K, default: _T) -> Union[_V, _T]: - ... - - # We need to override this explicitly, because our defaultdict-like behavior - # will make the default implementation (from our base class) always insert - # the key. - def get(self, key, default=None): - if key in self: - return self[key] - else: - return default - - def __setitem__(self, key: _K, value: _V) -> _T: - checked_key = self._key_checker.CheckValue(key) - checked_value = self._value_checker.CheckValue(value) - self._values[checked_key] = checked_value - self._message_listener.Modified() - - def __delitem__(self, key: _K) -> None: - del self._values[key] - self._message_listener.Modified() - - def __len__(self) -> int: - return len(self._values) - - def __iter__(self) -> Iterator[_K]: - return iter(self._values) - - def __repr__(self) -> str: - return repr(self._values) - - def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None: - self._values.update(other._values) - self._message_listener.Modified() - - def InvalidateIterators(self) -> None: - # It appears that the only way to reliably invalidate iterators to - # self._values is to ensure that its size changes. - original = self._values - self._values = original.copy() - original[None] = None - - # This is defined in the abstract base, but we can do it much more cheaply. - def clear(self) -> None: - self._values.clear() - self._message_listener.Modified() - - def GetEntryClass(self) -> Any: - return self._entry_descriptor._concrete_class - - -class MessageMap(MutableMapping[_K, _V]): - """Simple, type-checked, dict-like container for with submessage values.""" - - # Disallows assignment to other attributes. - __slots__ = ['_key_checker', '_values', '_message_listener', - '_message_descriptor', '_entry_descriptor'] - - def __init__( - self, - message_listener: Any, - message_descriptor: Any, - key_checker: Any, - entry_descriptor: Any, - ) -> None: - """ - Args: - message_listener: A MessageListener implementation. - The ScalarMap will call this object's Modified() method when it - is modified. - key_checker: A type_checkers.ValueChecker instance to run on keys - inserted into this container. - value_checker: A type_checkers.ValueChecker instance to run on values - inserted into this container. - entry_descriptor: The MessageDescriptor of a map entry: key and value. - """ - self._message_listener = message_listener - self._message_descriptor = message_descriptor - self._key_checker = key_checker - self._entry_descriptor = entry_descriptor - self._values = {} - - def __getitem__(self, key: _K) -> _V: - key = self._key_checker.CheckValue(key) - try: - return self._values[key] - except KeyError: - new_element = self._message_descriptor._concrete_class() - new_element._SetListener(self._message_listener) - self._values[key] = new_element - self._message_listener.Modified() - return new_element - - def get_or_create(self, key: _K) -> _V: - """get_or_create() is an alias for getitem (ie. map[key]). - - Args: - key: The key to get or create in the map. - - This is useful in cases where you want to be explicit that the call is - mutating the map. This can avoid lint errors for statements like this - that otherwise would appear to be pointless statements: - - msg.my_map[key] - """ - return self[key] - - @overload - def get(self, key: _K) -> Optional[_V]: - ... - - @overload - def get(self, key: _K, default: _T) -> Union[_V, _T]: - ... - - # We need to override this explicitly, because our defaultdict-like behavior - # will make the default implementation (from our base class) always insert - # the key. - def get(self, key, default=None): - if key in self: - return self[key] - else: - return default - - def __contains__(self, item: _K) -> bool: - item = self._key_checker.CheckValue(item) - return item in self._values - - def __setitem__(self, key: _K, value: _V) -> NoReturn: - raise ValueError('May not set values directly, call my_map[key].foo = 5') - - def __delitem__(self, key: _K) -> None: - key = self._key_checker.CheckValue(key) - del self._values[key] - self._message_listener.Modified() - - def __len__(self) -> int: - return len(self._values) - - def __iter__(self) -> Iterator[_K]: - return iter(self._values) - - def __repr__(self) -> str: - return repr(self._values) - - def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None: - # pylint: disable=protected-access - for key in other._values: - # According to documentation: "When parsing from the wire or when merging, - # if there are duplicate map keys the last key seen is used". - if key in self: - del self[key] - self[key].CopyFrom(other[key]) - # self._message_listener.Modified() not required here, because - # mutations to submessages already propagate. - - def InvalidateIterators(self) -> None: - # It appears that the only way to reliably invalidate iterators to - # self._values is to ensure that its size changes. - original = self._values - self._values = original.copy() - original[None] = None - - # This is defined in the abstract base, but we can do it much more cheaply. - def clear(self) -> None: - self._values.clear() - self._message_listener.Modified() - - def GetEntryClass(self) -> Any: - return self._entry_descriptor._concrete_class - - -class _UnknownField: - """A parsed unknown field.""" - - # Disallows assignment to other attributes. - __slots__ = ['_field_number', '_wire_type', '_data'] - - def __init__(self, field_number, wire_type, data): - self._field_number = field_number - self._wire_type = wire_type - self._data = data - return - - def __lt__(self, other): - # pylint: disable=protected-access - return self._field_number < other._field_number - - def __eq__(self, other): - if self is other: - return True - # pylint: disable=protected-access - return (self._field_number == other._field_number and - self._wire_type == other._wire_type and - self._data == other._data) - - -class UnknownFieldRef: # pylint: disable=missing-class-docstring - - def __init__(self, parent, index): - self._parent = parent - self._index = index - - def _check_valid(self): - if not self._parent: - raise ValueError('UnknownField does not exist. ' - 'The parent message might be cleared.') - if self._index >= len(self._parent): - raise ValueError('UnknownField does not exist. ' - 'The parent message might be cleared.') - - @property - def field_number(self): - self._check_valid() - # pylint: disable=protected-access - return self._parent._internal_get(self._index)._field_number - - @property - def wire_type(self): - self._check_valid() - # pylint: disable=protected-access - return self._parent._internal_get(self._index)._wire_type - - @property - def data(self): - self._check_valid() - # pylint: disable=protected-access - return self._parent._internal_get(self._index)._data - - -class UnknownFieldSet: - """UnknownField container""" - - # Disallows assignment to other attributes. - __slots__ = ['_values'] - - def __init__(self): - self._values = [] - - def __getitem__(self, index): - if self._values is None: - raise ValueError('UnknownFields does not exist. ' - 'The parent message might be cleared.') - size = len(self._values) - if index < 0: - index += size - if index < 0 or index >= size: - raise IndexError('index %d out of range'.index) - - return UnknownFieldRef(self, index) - - def _internal_get(self, index): - return self._values[index] - - def __len__(self): - if self._values is None: - raise ValueError('UnknownFields does not exist. ' - 'The parent message might be cleared.') - return len(self._values) - - def _add(self, field_number, wire_type, data): - unknown_field = _UnknownField(field_number, wire_type, data) - self._values.append(unknown_field) - return unknown_field - - def __iter__(self): - for i in range(len(self)): - yield UnknownFieldRef(self, i) - - def _extend(self, other): - if other is None: - return - # pylint: disable=protected-access - self._values.extend(other._values) - - def __eq__(self, other): - if self is other: - return True - # Sort unknown fields because their order shouldn't - # affect equality test. - values = list(self._values) - if other is None: - return not values - values.sort() - # pylint: disable=protected-access - other_values = sorted(other._values) - return values == other_values - - def _clear(self): - for value in self._values: - # pylint: disable=protected-access - if isinstance(value._data, UnknownFieldSet): - value._data._clear() # pylint: disable=protected-access - self._values = None diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/decoder.py b/script.module.protobuf/resources/lib/google/protobuf/internal/decoder.py deleted file mode 100644 index 8ff549381..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/decoder.py +++ /dev/null @@ -1,1067 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Code for decoding protocol buffer primitives. - -This code is very similar to encoder.py -- read the docs for that module first. - -A "decoder" is a function with the signature: - Decode(buffer, pos, end, message, field_dict) -The arguments are: - buffer: The string containing the encoded message. - pos: The current position in the string. - end: The position in the string where the current message ends. May be - less than len(buffer) if we're reading a sub-message. - message: The message object into which we're parsing. - field_dict: message._fields (avoids a hashtable lookup). -The decoder reads the field and stores it into field_dict, returning the new -buffer position. A decoder for a repeated field may proactively decode all of -the elements of that field, if they appear consecutively. - -Note that decoders may throw any of the following: - IndexError: Indicates a truncated message. - struct.error: Unpacking of a fixed-width field failed. - message.DecodeError: Other errors. - -Decoders are expected to raise an exception if they are called with pos > end. -This allows callers to be lax about bounds checking: it's fineto read past -"end" as long as you are sure that someone else will notice and throw an -exception later on. - -Something up the call stack is expected to catch IndexError and struct.error -and convert them to message.DecodeError. - -Decoders are constructed using decoder constructors with the signature: - MakeDecoder(field_number, is_repeated, is_packed, key, new_default) -The arguments are: - field_number: The field number of the field we want to decode. - is_repeated: Is the field a repeated field? (bool) - is_packed: Is the field a packed field? (bool) - key: The key to use when looking up the field within field_dict. - (This is actually the FieldDescriptor but nothing in this - file should depend on that.) - new_default: A function which takes a message object as a parameter and - returns a new instance of the default value for this field. - (This is called for repeated fields and sub-messages, when an - instance does not already exist.) - -As with encoders, we define a decoder constructor for every type of field. -Then, for every field of every message class we construct an actual decoder. -That decoder goes into a dict indexed by tag, so when we decode a message -we repeatedly read a tag, look up the corresponding decoder, and invoke it. -""" - -__author__ = 'kenton@google.com (Kenton Varda)' - -import math -import struct - -from google.protobuf.internal import containers -from google.protobuf.internal import encoder -from google.protobuf.internal import wire_format -from google.protobuf import message - - -# This is not for optimization, but rather to avoid conflicts with local -# variables named "message". -_DecodeError = message.DecodeError - - -def _VarintDecoder(mask, result_type): - """Return an encoder for a basic varint value (does not include tag). - - Decoded values will be bitwise-anded with the given mask before being - returned, e.g. to limit them to 32 bits. The returned decoder does not - take the usual "end" parameter -- the caller is expected to do bounds checking - after the fact (often the caller can defer such checking until later). The - decoder returns a (value, new_pos) pair. - """ - - def DecodeVarint(buffer, pos): - result = 0 - shift = 0 - while 1: - b = buffer[pos] - result |= ((b & 0x7f) << shift) - pos += 1 - if not (b & 0x80): - result &= mask - result = result_type(result) - return (result, pos) - shift += 7 - if shift >= 64: - raise _DecodeError('Too many bytes when decoding varint.') - return DecodeVarint - - -def _SignedVarintDecoder(bits, result_type): - """Like _VarintDecoder() but decodes signed values.""" - - signbit = 1 << (bits - 1) - mask = (1 << bits) - 1 - - def DecodeVarint(buffer, pos): - result = 0 - shift = 0 - while 1: - b = buffer[pos] - result |= ((b & 0x7f) << shift) - pos += 1 - if not (b & 0x80): - result &= mask - result = (result ^ signbit) - signbit - result = result_type(result) - return (result, pos) - shift += 7 - if shift >= 64: - raise _DecodeError('Too many bytes when decoding varint.') - return DecodeVarint - -# All 32-bit and 64-bit values are represented as int. -_DecodeVarint = _VarintDecoder((1 << 64) - 1, int) -_DecodeSignedVarint = _SignedVarintDecoder(64, int) - -# Use these versions for values which must be limited to 32 bits. -_DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int) -_DecodeSignedVarint32 = _SignedVarintDecoder(32, int) - - -def ReadTag(buffer, pos): - """Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple. - - We return the raw bytes of the tag rather than decoding them. The raw - bytes can then be used to look up the proper decoder. This effectively allows - us to trade some work that would be done in pure-python (decoding a varint) - for work that is done in C (searching for a byte string in a hash table). - In a low-level language it would be much cheaper to decode the varint and - use that, but not in Python. - - Args: - buffer: memoryview object of the encoded bytes - pos: int of the current position to start from - - Returns: - Tuple[bytes, int] of the tag data and new position. - """ - start = pos - while buffer[pos] & 0x80: - pos += 1 - pos += 1 - - tag_bytes = buffer[start:pos].tobytes() - return tag_bytes, pos - - -# -------------------------------------------------------------------- - - -def _SimpleDecoder(wire_type, decode_value): - """Return a constructor for a decoder for fields of a particular type. - - Args: - wire_type: The field's wire type. - decode_value: A function which decodes an individual value, e.g. - _DecodeVarint() - """ - - def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default, - clear_if_default=False): - if is_packed: - local_DecodeVarint = _DecodeVarint - def DecodePackedField(buffer, pos, end, message, field_dict): - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - (endpoint, pos) = local_DecodeVarint(buffer, pos) - endpoint += pos - if endpoint > end: - raise _DecodeError('Truncated message.') - while pos < endpoint: - (element, pos) = decode_value(buffer, pos) - value.append(element) - if pos > endpoint: - del value[-1] # Discard corrupt value. - raise _DecodeError('Packed element was truncated.') - return pos - return DecodePackedField - elif is_repeated: - tag_bytes = encoder.TagBytes(field_number, wire_type) - tag_len = len(tag_bytes) - def DecodeRepeatedField(buffer, pos, end, message, field_dict): - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - while 1: - (element, new_pos) = decode_value(buffer, pos) - value.append(element) - # Predict that the next tag is another copy of the same repeated - # field. - pos = new_pos + tag_len - if buffer[new_pos:pos] != tag_bytes or new_pos >= end: - # Prediction failed. Return. - if new_pos > end: - raise _DecodeError('Truncated message.') - return new_pos - return DecodeRepeatedField - else: - def DecodeField(buffer, pos, end, message, field_dict): - (new_value, pos) = decode_value(buffer, pos) - if pos > end: - raise _DecodeError('Truncated message.') - if clear_if_default and not new_value: - field_dict.pop(key, None) - else: - field_dict[key] = new_value - return pos - return DecodeField - - return SpecificDecoder - - -def _ModifiedDecoder(wire_type, decode_value, modify_value): - """Like SimpleDecoder but additionally invokes modify_value on every value - before storing it. Usually modify_value is ZigZagDecode. - """ - - # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but - # not enough to make a significant difference. - - def InnerDecode(buffer, pos): - (result, new_pos) = decode_value(buffer, pos) - return (modify_value(result), new_pos) - return _SimpleDecoder(wire_type, InnerDecode) - - -def _StructPackDecoder(wire_type, format): - """Return a constructor for a decoder for a fixed-width field. - - Args: - wire_type: The field's wire type. - format: The format string to pass to struct.unpack(). - """ - - value_size = struct.calcsize(format) - local_unpack = struct.unpack - - # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but - # not enough to make a significant difference. - - # Note that we expect someone up-stack to catch struct.error and convert - # it to _DecodeError -- this way we don't have to set up exception- - # handling blocks every time we parse one value. - - def InnerDecode(buffer, pos): - new_pos = pos + value_size - result = local_unpack(format, buffer[pos:new_pos])[0] - return (result, new_pos) - return _SimpleDecoder(wire_type, InnerDecode) - - -def _FloatDecoder(): - """Returns a decoder for a float field. - - This code works around a bug in struct.unpack for non-finite 32-bit - floating-point values. - """ - - local_unpack = struct.unpack - - def InnerDecode(buffer, pos): - """Decode serialized float to a float and new position. - - Args: - buffer: memoryview of the serialized bytes - pos: int, position in the memory view to start at. - - Returns: - Tuple[float, int] of the deserialized float value and new position - in the serialized data. - """ - # We expect a 32-bit value in little-endian byte order. Bit 1 is the sign - # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand. - new_pos = pos + 4 - float_bytes = buffer[pos:new_pos].tobytes() - - # If this value has all its exponent bits set, then it's non-finite. - # In Python 2.4, struct.unpack will convert it to a finite 64-bit value. - # To avoid that, we parse it specially. - if (float_bytes[3:4] in b'\x7F\xFF' and float_bytes[2:3] >= b'\x80'): - # If at least one significand bit is set... - if float_bytes[0:3] != b'\x00\x00\x80': - return (math.nan, new_pos) - # If sign bit is set... - if float_bytes[3:4] == b'\xFF': - return (-math.inf, new_pos) - return (math.inf, new_pos) - - # Note that we expect someone up-stack to catch struct.error and convert - # it to _DecodeError -- this way we don't have to set up exception- - # handling blocks every time we parse one value. - result = local_unpack('= b'\xF0') - and (double_bytes[0:7] != b'\x00\x00\x00\x00\x00\x00\xF0')): - return (math.nan, new_pos) - - # Note that we expect someone up-stack to catch struct.error and convert - # it to _DecodeError -- this way we don't have to set up exception- - # handling blocks every time we parse one value. - result = local_unpack(' end: - raise _DecodeError('Truncated message.') - while pos < endpoint: - value_start_pos = pos - (element, pos) = _DecodeSignedVarint32(buffer, pos) - # pylint: disable=protected-access - if element in enum_type.values_by_number: - value.append(element) - else: - if not message._unknown_fields: - message._unknown_fields = [] - tag_bytes = encoder.TagBytes(field_number, - wire_format.WIRETYPE_VARINT) - - message._unknown_fields.append( - (tag_bytes, buffer[value_start_pos:pos].tobytes())) - if message._unknown_field_set is None: - message._unknown_field_set = containers.UnknownFieldSet() - message._unknown_field_set._add( - field_number, wire_format.WIRETYPE_VARINT, element) - # pylint: enable=protected-access - if pos > endpoint: - if element in enum_type.values_by_number: - del value[-1] # Discard corrupt value. - else: - del message._unknown_fields[-1] - # pylint: disable=protected-access - del message._unknown_field_set._values[-1] - # pylint: enable=protected-access - raise _DecodeError('Packed element was truncated.') - return pos - return DecodePackedField - elif is_repeated: - tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) - tag_len = len(tag_bytes) - def DecodeRepeatedField(buffer, pos, end, message, field_dict): - """Decode serialized repeated enum to its value and a new position. - - Args: - buffer: memoryview of the serialized bytes. - pos: int, position in the memory view to start at. - end: int, end position of serialized data - message: Message object to store unknown fields in - field_dict: Map[Descriptor, Any] to store decoded values in. - - Returns: - int, new position in serialized data. - """ - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - while 1: - (element, new_pos) = _DecodeSignedVarint32(buffer, pos) - # pylint: disable=protected-access - if element in enum_type.values_by_number: - value.append(element) - else: - if not message._unknown_fields: - message._unknown_fields = [] - message._unknown_fields.append( - (tag_bytes, buffer[pos:new_pos].tobytes())) - if message._unknown_field_set is None: - message._unknown_field_set = containers.UnknownFieldSet() - message._unknown_field_set._add( - field_number, wire_format.WIRETYPE_VARINT, element) - # pylint: enable=protected-access - # Predict that the next tag is another copy of the same repeated - # field. - pos = new_pos + tag_len - if buffer[new_pos:pos] != tag_bytes or new_pos >= end: - # Prediction failed. Return. - if new_pos > end: - raise _DecodeError('Truncated message.') - return new_pos - return DecodeRepeatedField - else: - def DecodeField(buffer, pos, end, message, field_dict): - """Decode serialized repeated enum to its value and a new position. - - Args: - buffer: memoryview of the serialized bytes. - pos: int, position in the memory view to start at. - end: int, end position of serialized data - message: Message object to store unknown fields in - field_dict: Map[Descriptor, Any] to store decoded values in. - - Returns: - int, new position in serialized data. - """ - value_start_pos = pos - (enum_value, pos) = _DecodeSignedVarint32(buffer, pos) - if pos > end: - raise _DecodeError('Truncated message.') - if clear_if_default and not enum_value: - field_dict.pop(key, None) - return pos - # pylint: disable=protected-access - if enum_value in enum_type.values_by_number: - field_dict[key] = enum_value - else: - if not message._unknown_fields: - message._unknown_fields = [] - tag_bytes = encoder.TagBytes(field_number, - wire_format.WIRETYPE_VARINT) - message._unknown_fields.append( - (tag_bytes, buffer[value_start_pos:pos].tobytes())) - if message._unknown_field_set is None: - message._unknown_field_set = containers.UnknownFieldSet() - message._unknown_field_set._add( - field_number, wire_format.WIRETYPE_VARINT, enum_value) - # pylint: enable=protected-access - return pos - return DecodeField - - -# -------------------------------------------------------------------- - - -Int32Decoder = _SimpleDecoder( - wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32) - -Int64Decoder = _SimpleDecoder( - wire_format.WIRETYPE_VARINT, _DecodeSignedVarint) - -UInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32) -UInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint) - -SInt32Decoder = _ModifiedDecoder( - wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode) -SInt64Decoder = _ModifiedDecoder( - wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode) - -# Note that Python conveniently guarantees that when using the '<' prefix on -# formats, they will also have the same size across all platforms (as opposed -# to without the prefix, where their sizes depend on the C compiler's basic -# type sizes). -Fixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, ' end: - raise _DecodeError('Truncated string.') - value.append(_ConvertToUnicode(buffer[pos:new_pos])) - # Predict that the next tag is another copy of the same repeated field. - pos = new_pos + tag_len - if buffer[new_pos:pos] != tag_bytes or new_pos == end: - # Prediction failed. Return. - return new_pos - return DecodeRepeatedField - else: - def DecodeField(buffer, pos, end, message, field_dict): - (size, pos) = local_DecodeVarint(buffer, pos) - new_pos = pos + size - if new_pos > end: - raise _DecodeError('Truncated string.') - if clear_if_default and not size: - field_dict.pop(key, None) - else: - field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos]) - return new_pos - return DecodeField - - -def BytesDecoder(field_number, is_repeated, is_packed, key, new_default, - clear_if_default=False): - """Returns a decoder for a bytes field.""" - - local_DecodeVarint = _DecodeVarint - - assert not is_packed - if is_repeated: - tag_bytes = encoder.TagBytes(field_number, - wire_format.WIRETYPE_LENGTH_DELIMITED) - tag_len = len(tag_bytes) - def DecodeRepeatedField(buffer, pos, end, message, field_dict): - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - while 1: - (size, pos) = local_DecodeVarint(buffer, pos) - new_pos = pos + size - if new_pos > end: - raise _DecodeError('Truncated string.') - value.append(buffer[pos:new_pos].tobytes()) - # Predict that the next tag is another copy of the same repeated field. - pos = new_pos + tag_len - if buffer[new_pos:pos] != tag_bytes or new_pos == end: - # Prediction failed. Return. - return new_pos - return DecodeRepeatedField - else: - def DecodeField(buffer, pos, end, message, field_dict): - (size, pos) = local_DecodeVarint(buffer, pos) - new_pos = pos + size - if new_pos > end: - raise _DecodeError('Truncated string.') - if clear_if_default and not size: - field_dict.pop(key, None) - else: - field_dict[key] = buffer[pos:new_pos].tobytes() - return new_pos - return DecodeField - - -def GroupDecoder(field_number, is_repeated, is_packed, key, new_default): - """Returns a decoder for a group field.""" - - end_tag_bytes = encoder.TagBytes(field_number, - wire_format.WIRETYPE_END_GROUP) - end_tag_len = len(end_tag_bytes) - - assert not is_packed - if is_repeated: - tag_bytes = encoder.TagBytes(field_number, - wire_format.WIRETYPE_START_GROUP) - tag_len = len(tag_bytes) - def DecodeRepeatedField(buffer, pos, end, message, field_dict): - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - while 1: - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - # Read sub-message. - pos = value.add()._InternalParse(buffer, pos, end) - # Read end tag. - new_pos = pos+end_tag_len - if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: - raise _DecodeError('Missing group end tag.') - # Predict that the next tag is another copy of the same repeated field. - pos = new_pos + tag_len - if buffer[new_pos:pos] != tag_bytes or new_pos == end: - # Prediction failed. Return. - return new_pos - return DecodeRepeatedField - else: - def DecodeField(buffer, pos, end, message, field_dict): - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - # Read sub-message. - pos = value._InternalParse(buffer, pos, end) - # Read end tag. - new_pos = pos+end_tag_len - if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: - raise _DecodeError('Missing group end tag.') - return new_pos - return DecodeField - - -def MessageDecoder(field_number, is_repeated, is_packed, key, new_default): - """Returns a decoder for a message field.""" - - local_DecodeVarint = _DecodeVarint - - assert not is_packed - if is_repeated: - tag_bytes = encoder.TagBytes(field_number, - wire_format.WIRETYPE_LENGTH_DELIMITED) - tag_len = len(tag_bytes) - def DecodeRepeatedField(buffer, pos, end, message, field_dict): - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - while 1: - # Read length. - (size, pos) = local_DecodeVarint(buffer, pos) - new_pos = pos + size - if new_pos > end: - raise _DecodeError('Truncated message.') - # Read sub-message. - if value.add()._InternalParse(buffer, pos, new_pos) != new_pos: - # The only reason _InternalParse would return early is if it - # encountered an end-group tag. - raise _DecodeError('Unexpected end-group tag.') - # Predict that the next tag is another copy of the same repeated field. - pos = new_pos + tag_len - if buffer[new_pos:pos] != tag_bytes or new_pos == end: - # Prediction failed. Return. - return new_pos - return DecodeRepeatedField - else: - def DecodeField(buffer, pos, end, message, field_dict): - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - # Read length. - (size, pos) = local_DecodeVarint(buffer, pos) - new_pos = pos + size - if new_pos > end: - raise _DecodeError('Truncated message.') - # Read sub-message. - if value._InternalParse(buffer, pos, new_pos) != new_pos: - # The only reason _InternalParse would return early is if it encountered - # an end-group tag. - raise _DecodeError('Unexpected end-group tag.') - return new_pos - return DecodeField - - -# -------------------------------------------------------------------- - -MESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP) - -def MessageSetItemDecoder(descriptor): - """Returns a decoder for a MessageSet item. - - The parameter is the message Descriptor. - - The message set message looks like this: - message MessageSet { - repeated group Item = 1 { - required int32 type_id = 2; - required string message = 3; - } - } - """ - - type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) - message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) - item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) - - local_ReadTag = ReadTag - local_DecodeVarint = _DecodeVarint - local_SkipField = SkipField - - def DecodeItem(buffer, pos, end, message, field_dict): - """Decode serialized message set to its value and new position. - - Args: - buffer: memoryview of the serialized bytes. - pos: int, position in the memory view to start at. - end: int, end position of serialized data - message: Message object to store unknown fields in - field_dict: Map[Descriptor, Any] to store decoded values in. - - Returns: - int, new position in serialized data. - """ - message_set_item_start = pos - type_id = -1 - message_start = -1 - message_end = -1 - - # Technically, type_id and message can appear in any order, so we need - # a little loop here. - while 1: - (tag_bytes, pos) = local_ReadTag(buffer, pos) - if tag_bytes == type_id_tag_bytes: - (type_id, pos) = local_DecodeVarint(buffer, pos) - elif tag_bytes == message_tag_bytes: - (size, message_start) = local_DecodeVarint(buffer, pos) - pos = message_end = message_start + size - elif tag_bytes == item_end_tag_bytes: - break - else: - pos = SkipField(buffer, pos, end, tag_bytes) - if pos == -1: - raise _DecodeError('Missing group end tag.') - - if pos > end: - raise _DecodeError('Truncated message.') - - if type_id == -1: - raise _DecodeError('MessageSet item missing type_id.') - if message_start == -1: - raise _DecodeError('MessageSet item missing message.') - - extension = message.Extensions._FindExtensionByNumber(type_id) - # pylint: disable=protected-access - if extension is not None: - value = field_dict.get(extension) - if value is None: - message_type = extension.message_type - if not hasattr(message_type, '_concrete_class'): - message_factory.GetMessageClass(message_type) - value = field_dict.setdefault( - extension, message_type._concrete_class()) - if value._InternalParse(buffer, message_start,message_end) != message_end: - # The only reason _InternalParse would return early is if it encountered - # an end-group tag. - raise _DecodeError('Unexpected end-group tag.') - else: - if not message._unknown_fields: - message._unknown_fields = [] - message._unknown_fields.append( - (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes())) - if message._unknown_field_set is None: - message._unknown_field_set = containers.UnknownFieldSet() - message._unknown_field_set._add( - type_id, - wire_format.WIRETYPE_LENGTH_DELIMITED, - buffer[message_start:message_end].tobytes()) - # pylint: enable=protected-access - - return pos - - return DecodeItem - - -def UnknownMessageSetItemDecoder(): - """Returns a decoder for a Unknown MessageSet item.""" - - type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) - message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) - item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) - - def DecodeUnknownItem(buffer): - pos = 0 - end = len(buffer) - message_start = -1 - message_end = -1 - while 1: - (tag_bytes, pos) = ReadTag(buffer, pos) - if tag_bytes == type_id_tag_bytes: - (type_id, pos) = _DecodeVarint(buffer, pos) - elif tag_bytes == message_tag_bytes: - (size, message_start) = _DecodeVarint(buffer, pos) - pos = message_end = message_start + size - elif tag_bytes == item_end_tag_bytes: - break - else: - pos = SkipField(buffer, pos, end, tag_bytes) - if pos == -1: - raise _DecodeError('Missing group end tag.') - - if pos > end: - raise _DecodeError('Truncated message.') - - if type_id == -1: - raise _DecodeError('MessageSet item missing type_id.') - if message_start == -1: - raise _DecodeError('MessageSet item missing message.') - - return (type_id, buffer[message_start:message_end].tobytes()) - - return DecodeUnknownItem - -# -------------------------------------------------------------------- - -def MapDecoder(field_descriptor, new_default, is_message_map): - """Returns a decoder for a map field.""" - - key = field_descriptor - tag_bytes = encoder.TagBytes(field_descriptor.number, - wire_format.WIRETYPE_LENGTH_DELIMITED) - tag_len = len(tag_bytes) - local_DecodeVarint = _DecodeVarint - # Can't read _concrete_class yet; might not be initialized. - message_type = field_descriptor.message_type - - def DecodeMap(buffer, pos, end, message, field_dict): - submsg = message_type._concrete_class() - value = field_dict.get(key) - if value is None: - value = field_dict.setdefault(key, new_default(message)) - while 1: - # Read length. - (size, pos) = local_DecodeVarint(buffer, pos) - new_pos = pos + size - if new_pos > end: - raise _DecodeError('Truncated message.') - # Read sub-message. - submsg.Clear() - if submsg._InternalParse(buffer, pos, new_pos) != new_pos: - # The only reason _InternalParse would return early is if it - # encountered an end-group tag. - raise _DecodeError('Unexpected end-group tag.') - - if is_message_map: - value[submsg.key].CopyFrom(submsg.value) - else: - value[submsg.key] = submsg.value - - # Predict that the next tag is another copy of the same repeated field. - pos = new_pos + tag_len - if buffer[new_pos:pos] != tag_bytes or new_pos == end: - # Prediction failed. Return. - return new_pos - - return DecodeMap - -# -------------------------------------------------------------------- -# Optimization is not as heavy here because calls to SkipField() are rare, -# except for handling end-group tags. - -def _SkipVarint(buffer, pos, end): - """Skip a varint value. Returns the new position.""" - # Previously ord(buffer[pos]) raised IndexError when pos is out of range. - # With this code, ord(b'') raises TypeError. Both are handled in - # python_message.py to generate a 'Truncated message' error. - while ord(buffer[pos:pos+1].tobytes()) & 0x80: - pos += 1 - pos += 1 - if pos > end: - raise _DecodeError('Truncated message.') - return pos - -def _SkipFixed64(buffer, pos, end): - """Skip a fixed64 value. Returns the new position.""" - - pos += 8 - if pos > end: - raise _DecodeError('Truncated message.') - return pos - - -def _DecodeFixed64(buffer, pos): - """Decode a fixed64.""" - new_pos = pos + 8 - return (struct.unpack(' end: - raise _DecodeError('Truncated message.') - return pos - - -def _SkipGroup(buffer, pos, end): - """Skip sub-group. Returns the new position.""" - - while 1: - (tag_bytes, pos) = ReadTag(buffer, pos) - new_pos = SkipField(buffer, pos, end, tag_bytes) - if new_pos == -1: - return pos - pos = new_pos - - -def _DecodeUnknownFieldSet(buffer, pos, end_pos=None): - """Decode UnknownFieldSet. Returns the UnknownFieldSet and new position.""" - - unknown_field_set = containers.UnknownFieldSet() - while end_pos is None or pos < end_pos: - (tag_bytes, pos) = ReadTag(buffer, pos) - (tag, _) = _DecodeVarint(tag_bytes, 0) - field_number, wire_type = wire_format.UnpackTag(tag) - if wire_type == wire_format.WIRETYPE_END_GROUP: - break - (data, pos) = _DecodeUnknownField(buffer, pos, wire_type) - # pylint: disable=protected-access - unknown_field_set._add(field_number, wire_type, data) - - return (unknown_field_set, pos) - - -def _DecodeUnknownField(buffer, pos, wire_type): - """Decode a unknown field. Returns the UnknownField and new position.""" - - if wire_type == wire_format.WIRETYPE_VARINT: - (data, pos) = _DecodeVarint(buffer, pos) - elif wire_type == wire_format.WIRETYPE_FIXED64: - (data, pos) = _DecodeFixed64(buffer, pos) - elif wire_type == wire_format.WIRETYPE_FIXED32: - (data, pos) = _DecodeFixed32(buffer, pos) - elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED: - (size, pos) = _DecodeVarint(buffer, pos) - data = buffer[pos:pos+size].tobytes() - pos += size - elif wire_type == wire_format.WIRETYPE_START_GROUP: - (data, pos) = _DecodeUnknownFieldSet(buffer, pos) - elif wire_type == wire_format.WIRETYPE_END_GROUP: - return (0, -1) - else: - raise _DecodeError('Wrong wire type in tag.') - - return (data, pos) - - -def _EndGroup(buffer, pos, end): - """Skipping an END_GROUP tag returns -1 to tell the parent loop to break.""" - - return -1 - - -def _SkipFixed32(buffer, pos, end): - """Skip a fixed32 value. Returns the new position.""" - - pos += 4 - if pos > end: - raise _DecodeError('Truncated message.') - return pos - - -def _DecodeFixed32(buffer, pos): - """Decode a fixed32.""" - - new_pos = pos + 4 - return (struct.unpack('B').pack - - def EncodeVarint(write, value, unused_deterministic=None): - bits = value & 0x7f - value >>= 7 - while value: - write(local_int2byte(0x80|bits)) - bits = value & 0x7f - value >>= 7 - return write(local_int2byte(bits)) - - return EncodeVarint - - -def _SignedVarintEncoder(): - """Return an encoder for a basic signed varint value (does not include - tag).""" - - local_int2byte = struct.Struct('>B').pack - - def EncodeSignedVarint(write, value, unused_deterministic=None): - if value < 0: - value += (1 << 64) - bits = value & 0x7f - value >>= 7 - while value: - write(local_int2byte(0x80|bits)) - bits = value & 0x7f - value >>= 7 - return write(local_int2byte(bits)) - - return EncodeSignedVarint - - -_EncodeVarint = _VarintEncoder() -_EncodeSignedVarint = _SignedVarintEncoder() - - -def _VarintBytes(value): - """Encode the given integer as a varint and return the bytes. This is only - called at startup time so it doesn't need to be fast.""" - - pieces = [] - _EncodeVarint(pieces.append, value, True) - return b"".join(pieces) - - -def TagBytes(field_number, wire_type): - """Encode the given tag and return the bytes. Only called at startup.""" - - return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type))) - -# -------------------------------------------------------------------- -# As with sizers (see above), we have a number of common encoder -# implementations. - - -def _SimpleEncoder(wire_type, encode_value, compute_value_size): - """Return a constructor for an encoder for fields of a particular type. - - Args: - wire_type: The field's wire type, for encoding tags. - encode_value: A function which encodes an individual value, e.g. - _EncodeVarint(). - compute_value_size: A function which computes the size of an individual - value, e.g. _VarintSize(). - """ - - def SpecificEncoder(field_number, is_repeated, is_packed): - if is_packed: - tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) - local_EncodeVarint = _EncodeVarint - def EncodePackedField(write, value, deterministic): - write(tag_bytes) - size = 0 - for element in value: - size += compute_value_size(element) - local_EncodeVarint(write, size, deterministic) - for element in value: - encode_value(write, element, deterministic) - return EncodePackedField - elif is_repeated: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeRepeatedField(write, value, deterministic): - for element in value: - write(tag_bytes) - encode_value(write, element, deterministic) - return EncodeRepeatedField - else: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeField(write, value, deterministic): - write(tag_bytes) - return encode_value(write, value, deterministic) - return EncodeField - - return SpecificEncoder - - -def _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value): - """Like SimpleEncoder but additionally invokes modify_value on every value - before passing it to encode_value. Usually modify_value is ZigZagEncode.""" - - def SpecificEncoder(field_number, is_repeated, is_packed): - if is_packed: - tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) - local_EncodeVarint = _EncodeVarint - def EncodePackedField(write, value, deterministic): - write(tag_bytes) - size = 0 - for element in value: - size += compute_value_size(modify_value(element)) - local_EncodeVarint(write, size, deterministic) - for element in value: - encode_value(write, modify_value(element), deterministic) - return EncodePackedField - elif is_repeated: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeRepeatedField(write, value, deterministic): - for element in value: - write(tag_bytes) - encode_value(write, modify_value(element), deterministic) - return EncodeRepeatedField - else: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeField(write, value, deterministic): - write(tag_bytes) - return encode_value(write, modify_value(value), deterministic) - return EncodeField - - return SpecificEncoder - - -def _StructPackEncoder(wire_type, format): - """Return a constructor for an encoder for a fixed-width field. - - Args: - wire_type: The field's wire type, for encoding tags. - format: The format string to pass to struct.pack(). - """ - - value_size = struct.calcsize(format) - - def SpecificEncoder(field_number, is_repeated, is_packed): - local_struct_pack = struct.pack - if is_packed: - tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) - local_EncodeVarint = _EncodeVarint - def EncodePackedField(write, value, deterministic): - write(tag_bytes) - local_EncodeVarint(write, len(value) * value_size, deterministic) - for element in value: - write(local_struct_pack(format, element)) - return EncodePackedField - elif is_repeated: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeRepeatedField(write, value, unused_deterministic=None): - for element in value: - write(tag_bytes) - write(local_struct_pack(format, element)) - return EncodeRepeatedField - else: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeField(write, value, unused_deterministic=None): - write(tag_bytes) - return write(local_struct_pack(format, value)) - return EncodeField - - return SpecificEncoder - - -def _FloatingPointEncoder(wire_type, format): - """Return a constructor for an encoder for float fields. - - This is like StructPackEncoder, but catches errors that may be due to - passing non-finite floating-point values to struct.pack, and makes a - second attempt to encode those values. - - Args: - wire_type: The field's wire type, for encoding tags. - format: The format string to pass to struct.pack(). - """ - - value_size = struct.calcsize(format) - if value_size == 4: - def EncodeNonFiniteOrRaise(write, value): - # Remember that the serialized form uses little-endian byte order. - if value == _POS_INF: - write(b'\x00\x00\x80\x7F') - elif value == _NEG_INF: - write(b'\x00\x00\x80\xFF') - elif value != value: # NaN - write(b'\x00\x00\xC0\x7F') - else: - raise - elif value_size == 8: - def EncodeNonFiniteOrRaise(write, value): - if value == _POS_INF: - write(b'\x00\x00\x00\x00\x00\x00\xF0\x7F') - elif value == _NEG_INF: - write(b'\x00\x00\x00\x00\x00\x00\xF0\xFF') - elif value != value: # NaN - write(b'\x00\x00\x00\x00\x00\x00\xF8\x7F') - else: - raise - else: - raise ValueError('Can\'t encode floating-point values that are ' - '%d bytes long (only 4 or 8)' % value_size) - - def SpecificEncoder(field_number, is_repeated, is_packed): - local_struct_pack = struct.pack - if is_packed: - tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) - local_EncodeVarint = _EncodeVarint - def EncodePackedField(write, value, deterministic): - write(tag_bytes) - local_EncodeVarint(write, len(value) * value_size, deterministic) - for element in value: - # This try/except block is going to be faster than any code that - # we could write to check whether element is finite. - try: - write(local_struct_pack(format, element)) - except SystemError: - EncodeNonFiniteOrRaise(write, element) - return EncodePackedField - elif is_repeated: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeRepeatedField(write, value, unused_deterministic=None): - for element in value: - write(tag_bytes) - try: - write(local_struct_pack(format, element)) - except SystemError: - EncodeNonFiniteOrRaise(write, element) - return EncodeRepeatedField - else: - tag_bytes = TagBytes(field_number, wire_type) - def EncodeField(write, value, unused_deterministic=None): - write(tag_bytes) - try: - write(local_struct_pack(format, value)) - except SystemError: - EncodeNonFiniteOrRaise(write, value) - return EncodeField - - return SpecificEncoder - - -# ==================================================================== -# Here we declare an encoder constructor for each field type. These work -# very similarly to sizer constructors, described earlier. - - -Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder( - wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize) - -UInt32Encoder = UInt64Encoder = _SimpleEncoder( - wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize) - -SInt32Encoder = SInt64Encoder = _ModifiedEncoder( - wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize, - wire_format.ZigZagEncode) - -# Note that Python conveniently guarantees that when using the '<' prefix on -# formats, they will also have the same size across all platforms (as opposed -# to without the prefix, where their sizes depend on the C compiler's basic -# type sizes). -Fixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, ' str - ValueType = int - - def __init__(self, enum_type): - """Inits EnumTypeWrapper with an EnumDescriptor.""" - self._enum_type = enum_type - self.DESCRIPTOR = enum_type # pylint: disable=invalid-name - - def Name(self, number): # pylint: disable=invalid-name - """Returns a string containing the name of an enum value.""" - try: - return self._enum_type.values_by_number[number].name - except KeyError: - pass # fall out to break exception chaining - - if not isinstance(number, int): - raise TypeError( - 'Enum value for {} must be an int, but got {} {!r}.'.format( - self._enum_type.name, type(number), number)) - else: - # repr here to handle the odd case when you pass in a boolean. - raise ValueError('Enum {} has no name defined for value {!r}'.format( - self._enum_type.name, number)) - - def Value(self, name): # pylint: disable=invalid-name - """Returns the value corresponding to the given enum name.""" - try: - return self._enum_type.values_by_name[name].number - except KeyError: - pass # fall out to break exception chaining - raise ValueError('Enum {} has no value defined for name {!r}'.format( - self._enum_type.name, name)) - - def keys(self): - """Return a list of the string names in the enum. - - Returns: - A list of strs, in the order they were defined in the .proto file. - """ - - return [value_descriptor.name - for value_descriptor in self._enum_type.values] - - def values(self): - """Return a list of the integer values in the enum. - - Returns: - A list of ints, in the order they were defined in the .proto file. - """ - - return [value_descriptor.number - for value_descriptor in self._enum_type.values] - - def items(self): - """Return a list of the (name, value) pairs of the enum. - - Returns: - A list of (str, int) pairs, in the order they were defined - in the .proto file. - """ - return [(value_descriptor.name, value_descriptor.number) - for value_descriptor in self._enum_type.values] - - def __getattr__(self, name): - """Returns the value corresponding to the given enum name.""" - try: - return super( - EnumTypeWrapper, - self).__getattribute__('_enum_type').values_by_name[name].number - except KeyError: - pass # fall out to break exception chaining - raise AttributeError('Enum {} has no value defined for name {!r}'.format( - self._enum_type.name, name)) diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/extension_dict.py b/script.module.protobuf/resources/lib/google/protobuf/internal/extension_dict.py deleted file mode 100644 index 83c4cb5dc..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/extension_dict.py +++ /dev/null @@ -1,214 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains _ExtensionDict class to represent extensions. -""" - -from google.protobuf.internal import type_checkers -from google.protobuf.descriptor import FieldDescriptor - - -def _VerifyExtensionHandle(message, extension_handle): - """Verify that the given extension handle is valid.""" - - if not isinstance(extension_handle, FieldDescriptor): - raise KeyError('HasExtension() expects an extension handle, got: %s' % - extension_handle) - - if not extension_handle.is_extension: - raise KeyError('"%s" is not an extension.' % extension_handle.full_name) - - if not extension_handle.containing_type: - raise KeyError('"%s" is missing a containing_type.' - % extension_handle.full_name) - - if extension_handle.containing_type is not message.DESCRIPTOR: - raise KeyError('Extension "%s" extends message type "%s", but this ' - 'message is of type "%s".' % - (extension_handle.full_name, - extension_handle.containing_type.full_name, - message.DESCRIPTOR.full_name)) - - -# TODO(robinson): Unify error handling of "unknown extension" crap. -# TODO(robinson): Support iteritems()-style iteration over all -# extensions with the "has" bits turned on? -class _ExtensionDict(object): - - """Dict-like container for Extension fields on proto instances. - - Note that in all cases we expect extension handles to be - FieldDescriptors. - """ - - def __init__(self, extended_message): - """ - Args: - extended_message: Message instance for which we are the Extensions dict. - """ - self._extended_message = extended_message - - def __getitem__(self, extension_handle): - """Returns the current value of the given extension handle.""" - - _VerifyExtensionHandle(self._extended_message, extension_handle) - - result = self._extended_message._fields.get(extension_handle) - if result is not None: - return result - - if extension_handle.label == FieldDescriptor.LABEL_REPEATED: - result = extension_handle._default_constructor(self._extended_message) - elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: - message_type = extension_handle.message_type - if not hasattr(message_type, '_concrete_class'): - # pylint: disable=g-import-not-at-top - from google.protobuf import message_factory - message_factory.GetMessageClass(message_type) - assert getattr(extension_handle.message_type, '_concrete_class', None), ( - 'Uninitialized concrete class found for field %r (message type %r)' - % (extension_handle.full_name, - extension_handle.message_type.full_name)) - result = extension_handle.message_type._concrete_class() - try: - result._SetListener(self._extended_message._listener_for_children) - except ReferenceError: - pass - else: - # Singular scalar -- just return the default without inserting into the - # dict. - return extension_handle.default_value - - # Atomically check if another thread has preempted us and, if not, swap - # in the new object we just created. If someone has preempted us, we - # take that object and discard ours. - # WARNING: We are relying on setdefault() being atomic. This is true - # in CPython but we haven't investigated others. This warning appears - # in several other locations in this file. - result = self._extended_message._fields.setdefault( - extension_handle, result) - - return result - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - - my_fields = self._extended_message.ListFields() - other_fields = other._extended_message.ListFields() - - # Get rid of non-extension fields. - my_fields = [field for field in my_fields if field.is_extension] - other_fields = [field for field in other_fields if field.is_extension] - - return my_fields == other_fields - - def __ne__(self, other): - return not self == other - - def __len__(self): - fields = self._extended_message.ListFields() - # Get rid of non-extension fields. - extension_fields = [field for field in fields if field[0].is_extension] - return len(extension_fields) - - def __hash__(self): - raise TypeError('unhashable object') - - # Note that this is only meaningful for non-repeated, scalar extension - # fields. Note also that we may have to call _Modified() when we do - # successfully set a field this way, to set any necessary "has" bits in the - # ancestors of the extended message. - def __setitem__(self, extension_handle, value): - """If extension_handle specifies a non-repeated, scalar extension - field, sets the value of that field. - """ - - _VerifyExtensionHandle(self._extended_message, extension_handle) - - if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or - extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE): - raise TypeError( - 'Cannot assign to extension "%s" because it is a repeated or ' - 'composite type.' % extension_handle.full_name) - - # It's slightly wasteful to lookup the type checker each time, - # but we expect this to be a vanishingly uncommon case anyway. - type_checker = type_checkers.GetTypeChecker(extension_handle) - # pylint: disable=protected-access - self._extended_message._fields[extension_handle] = ( - type_checker.CheckValue(value)) - self._extended_message._Modified() - - def __delitem__(self, extension_handle): - self._extended_message.ClearExtension(extension_handle) - - def _FindExtensionByName(self, name): - """Tries to find a known extension with the specified name. - - Args: - name: Extension full name. - - Returns: - Extension field descriptor. - """ - return self._extended_message._extensions_by_name.get(name, None) - - def _FindExtensionByNumber(self, number): - """Tries to find a known extension with the field number. - - Args: - number: Extension field number. - - Returns: - Extension field descriptor. - """ - return self._extended_message._extensions_by_number.get(number, None) - - def __iter__(self): - # Return a generator over the populated extension fields - return (f[0] for f in self._extended_message.ListFields() - if f[0].is_extension) - - def __contains__(self, extension_handle): - _VerifyExtensionHandle(self._extended_message, extension_handle) - - if extension_handle not in self._extended_message._fields: - return False - - if extension_handle.label == FieldDescriptor.LABEL_REPEATED: - return bool(self._extended_message._fields.get(extension_handle)) - - if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: - value = self._extended_message._fields.get(extension_handle) - # pylint: disable=protected-access - return value is not None and value._is_present_in_parent - - return True diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/field_mask.py b/script.module.protobuf/resources/lib/google/protobuf/internal/field_mask.py deleted file mode 100644 index 489769901..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/field_mask.py +++ /dev/null @@ -1,333 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains FieldMask class.""" - -from google.protobuf.descriptor import FieldDescriptor - - -class FieldMask(object): - """Class for FieldMask message type.""" - - __slots__ = () - - def ToJsonString(self): - """Converts FieldMask to string according to proto3 JSON spec.""" - camelcase_paths = [] - for path in self.paths: - camelcase_paths.append(_SnakeCaseToCamelCase(path)) - return ','.join(camelcase_paths) - - def FromJsonString(self, value): - """Converts string to FieldMask according to proto3 JSON spec.""" - if not isinstance(value, str): - raise ValueError('FieldMask JSON value not a string: {!r}'.format(value)) - self.Clear() - if value: - for path in value.split(','): - self.paths.append(_CamelCaseToSnakeCase(path)) - - def IsValidForDescriptor(self, message_descriptor): - """Checks whether the FieldMask is valid for Message Descriptor.""" - for path in self.paths: - if not _IsValidPath(message_descriptor, path): - return False - return True - - def AllFieldsFromDescriptor(self, message_descriptor): - """Gets all direct fields of Message Descriptor to FieldMask.""" - self.Clear() - for field in message_descriptor.fields: - self.paths.append(field.name) - - def CanonicalFormFromMask(self, mask): - """Converts a FieldMask to the canonical form. - - Removes paths that are covered by another path. For example, - "foo.bar" is covered by "foo" and will be removed if "foo" - is also in the FieldMask. Then sorts all paths in alphabetical order. - - Args: - mask: The original FieldMask to be converted. - """ - tree = _FieldMaskTree(mask) - tree.ToFieldMask(self) - - def Union(self, mask1, mask2): - """Merges mask1 and mask2 into this FieldMask.""" - _CheckFieldMaskMessage(mask1) - _CheckFieldMaskMessage(mask2) - tree = _FieldMaskTree(mask1) - tree.MergeFromFieldMask(mask2) - tree.ToFieldMask(self) - - def Intersect(self, mask1, mask2): - """Intersects mask1 and mask2 into this FieldMask.""" - _CheckFieldMaskMessage(mask1) - _CheckFieldMaskMessage(mask2) - tree = _FieldMaskTree(mask1) - intersection = _FieldMaskTree() - for path in mask2.paths: - tree.IntersectPath(path, intersection) - intersection.ToFieldMask(self) - - def MergeMessage( - self, source, destination, - replace_message_field=False, replace_repeated_field=False): - """Merges fields specified in FieldMask from source to destination. - - Args: - source: Source message. - destination: The destination message to be merged into. - replace_message_field: Replace message field if True. Merge message - field if False. - replace_repeated_field: Replace repeated field if True. Append - elements of repeated field if False. - """ - tree = _FieldMaskTree(self) - tree.MergeMessage( - source, destination, replace_message_field, replace_repeated_field) - - -def _IsValidPath(message_descriptor, path): - """Checks whether the path is valid for Message Descriptor.""" - parts = path.split('.') - last = parts.pop() - for name in parts: - field = message_descriptor.fields_by_name.get(name) - if (field is None or - field.label == FieldDescriptor.LABEL_REPEATED or - field.type != FieldDescriptor.TYPE_MESSAGE): - return False - message_descriptor = field.message_type - return last in message_descriptor.fields_by_name - - -def _CheckFieldMaskMessage(message): - """Raises ValueError if message is not a FieldMask.""" - message_descriptor = message.DESCRIPTOR - if (message_descriptor.name != 'FieldMask' or - message_descriptor.file.name != 'google/protobuf/field_mask.proto'): - raise ValueError('Message {0} is not a FieldMask.'.format( - message_descriptor.full_name)) - - -def _SnakeCaseToCamelCase(path_name): - """Converts a path name from snake_case to camelCase.""" - result = [] - after_underscore = False - for c in path_name: - if c.isupper(): - raise ValueError( - 'Fail to print FieldMask to Json string: Path name ' - '{0} must not contain uppercase letters.'.format(path_name)) - if after_underscore: - if c.islower(): - result.append(c.upper()) - after_underscore = False - else: - raise ValueError( - 'Fail to print FieldMask to Json string: The ' - 'character after a "_" must be a lowercase letter ' - 'in path name {0}.'.format(path_name)) - elif c == '_': - after_underscore = True - else: - result += c - - if after_underscore: - raise ValueError('Fail to print FieldMask to Json string: Trailing "_" ' - 'in path name {0}.'.format(path_name)) - return ''.join(result) - - -def _CamelCaseToSnakeCase(path_name): - """Converts a field name from camelCase to snake_case.""" - result = [] - for c in path_name: - if c == '_': - raise ValueError('Fail to parse FieldMask: Path name ' - '{0} must not contain "_"s.'.format(path_name)) - if c.isupper(): - result += '_' - result += c.lower() - else: - result += c - return ''.join(result) - - -class _FieldMaskTree(object): - """Represents a FieldMask in a tree structure. - - For example, given a FieldMask "foo.bar,foo.baz,bar.baz", - the FieldMaskTree will be: - [_root] -+- foo -+- bar - | | - | +- baz - | - +- bar --- baz - In the tree, each leaf node represents a field path. - """ - - __slots__ = ('_root',) - - def __init__(self, field_mask=None): - """Initializes the tree by FieldMask.""" - self._root = {} - if field_mask: - self.MergeFromFieldMask(field_mask) - - def MergeFromFieldMask(self, field_mask): - """Merges a FieldMask to the tree.""" - for path in field_mask.paths: - self.AddPath(path) - - def AddPath(self, path): - """Adds a field path into the tree. - - If the field path to add is a sub-path of an existing field path - in the tree (i.e., a leaf node), it means the tree already matches - the given path so nothing will be added to the tree. If the path - matches an existing non-leaf node in the tree, that non-leaf node - will be turned into a leaf node with all its children removed because - the path matches all the node's children. Otherwise, a new path will - be added. - - Args: - path: The field path to add. - """ - node = self._root - for name in path.split('.'): - if name not in node: - node[name] = {} - elif not node[name]: - # Pre-existing empty node implies we already have this entire tree. - return - node = node[name] - # Remove any sub-trees we might have had. - node.clear() - - def ToFieldMask(self, field_mask): - """Converts the tree to a FieldMask.""" - field_mask.Clear() - _AddFieldPaths(self._root, '', field_mask) - - def IntersectPath(self, path, intersection): - """Calculates the intersection part of a field path with this tree. - - Args: - path: The field path to calculates. - intersection: The out tree to record the intersection part. - """ - node = self._root - for name in path.split('.'): - if name not in node: - return - elif not node[name]: - intersection.AddPath(path) - return - node = node[name] - intersection.AddLeafNodes(path, node) - - def AddLeafNodes(self, prefix, node): - """Adds leaf nodes begin with prefix to this tree.""" - if not node: - self.AddPath(prefix) - for name in node: - child_path = prefix + '.' + name - self.AddLeafNodes(child_path, node[name]) - - def MergeMessage( - self, source, destination, - replace_message, replace_repeated): - """Merge all fields specified by this tree from source to destination.""" - _MergeMessage( - self._root, source, destination, replace_message, replace_repeated) - - -def _StrConvert(value): - """Converts value to str if it is not.""" - # This file is imported by c extension and some methods like ClearField - # requires string for the field name. py2/py3 has different text - # type and may use unicode. - if not isinstance(value, str): - return value.encode('utf-8') - return value - - -def _MergeMessage( - node, source, destination, replace_message, replace_repeated): - """Merge all fields specified by a sub-tree from source to destination.""" - source_descriptor = source.DESCRIPTOR - for name in node: - child = node[name] - field = source_descriptor.fields_by_name[name] - if field is None: - raise ValueError('Error: Can\'t find field {0} in message {1}.'.format( - name, source_descriptor.full_name)) - if child: - # Sub-paths are only allowed for singular message fields. - if (field.label == FieldDescriptor.LABEL_REPEATED or - field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE): - raise ValueError('Error: Field {0} in message {1} is not a singular ' - 'message field and cannot have sub-fields.'.format( - name, source_descriptor.full_name)) - if source.HasField(name): - _MergeMessage( - child, getattr(source, name), getattr(destination, name), - replace_message, replace_repeated) - continue - if field.label == FieldDescriptor.LABEL_REPEATED: - if replace_repeated: - destination.ClearField(_StrConvert(name)) - repeated_source = getattr(source, name) - repeated_destination = getattr(destination, name) - repeated_destination.MergeFrom(repeated_source) - else: - if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: - if replace_message: - destination.ClearField(_StrConvert(name)) - if source.HasField(name): - getattr(destination, name).MergeFrom(getattr(source, name)) - else: - setattr(destination, name, getattr(source, name)) - - -def _AddFieldPaths(node, prefix, field_mask): - """Adds the field paths descended from node to field_mask.""" - if not node and prefix: - field_mask.paths.append(prefix) - return - for name in sorted(node): - if prefix: - child_path = prefix + '.' + name - else: - child_path = name - _AddFieldPaths(node[name], child_path, field_mask) diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/message_listener.py b/script.module.protobuf/resources/lib/google/protobuf/internal/message_listener.py deleted file mode 100644 index 0fc255a77..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/message_listener.py +++ /dev/null @@ -1,78 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Defines a listener interface for observing certain -state transitions on Message objects. - -Also defines a null implementation of this interface. -""" - -__author__ = 'robinson@google.com (Will Robinson)' - - -class MessageListener(object): - - """Listens for modifications made to a message. Meant to be registered via - Message._SetListener(). - - Attributes: - dirty: If True, then calling Modified() would be a no-op. This can be - used to avoid these calls entirely in the common case. - """ - - def Modified(self): - """Called every time the message is modified in such a way that the parent - message may need to be updated. This currently means either: - (a) The message was modified for the first time, so the parent message - should henceforth mark the message as present. - (b) The message's cached byte size became dirty -- i.e. the message was - modified for the first time after a previous call to ByteSize(). - Therefore the parent should also mark its byte size as dirty. - Note that (a) implies (b), since new objects start out with a client cached - size (zero). However, we document (a) explicitly because it is important. - - Modified() will *only* be called in response to one of these two events -- - not every time the sub-message is modified. - - Note that if the listener's |dirty| attribute is true, then calling - Modified at the moment would be a no-op, so it can be skipped. Performance- - sensitive callers should check this attribute directly before calling since - it will be true most of the time. - """ - - raise NotImplementedError - - -class NullMessageListener(object): - - """No-op MessageListener implementation.""" - - def Modified(self): - pass diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/python_message.py b/script.module.protobuf/resources/lib/google/protobuf/internal/python_message.py deleted file mode 100644 index bf9acefd2..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/python_message.py +++ /dev/null @@ -1,1514 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# This code is meant to work on Python 2.4 and above only. -# -# TODO(robinson): Helpers for verbose, common checks like seeing if a -# descriptor's cpp_type is CPPTYPE_MESSAGE. - -"""Contains a metaclass and helper functions used to create -protocol message classes from Descriptor objects at runtime. - -Recall that a metaclass is the "type" of a class. -(A class is to a metaclass what an instance is to a class.) - -In this case, we use the GeneratedProtocolMessageType metaclass -to inject all the useful functionality into the classes -output by the protocol compiler at compile-time. - -The upshot of all this is that the real implementation -details for ALL pure-Python protocol buffers are *here in -this file*. -""" - -__author__ = 'robinson@google.com (Will Robinson)' - -from io import BytesIO -import struct -import sys -import weakref - -# We use "as" to avoid name collisions with variables. -from google.protobuf.internal import api_implementation -from google.protobuf.internal import containers -from google.protobuf.internal import decoder -from google.protobuf.internal import encoder -from google.protobuf.internal import enum_type_wrapper -from google.protobuf.internal import extension_dict -from google.protobuf.internal import message_listener as message_listener_mod -from google.protobuf.internal import type_checkers -from google.protobuf.internal import well_known_types -from google.protobuf.internal import wire_format -from google.protobuf import descriptor as descriptor_mod -from google.protobuf import message as message_mod -from google.protobuf import text_format - -_FieldDescriptor = descriptor_mod.FieldDescriptor -_AnyFullTypeName = 'google.protobuf.Any' -_ExtensionDict = extension_dict._ExtensionDict - -class GeneratedProtocolMessageType(type): - - """Metaclass for protocol message classes created at runtime from Descriptors. - - We add implementations for all methods described in the Message class. We - also create properties to allow getting/setting all fields in the protocol - message. Finally, we create slots to prevent users from accidentally - "setting" nonexistent fields in the protocol message, which then wouldn't get - serialized / deserialized properly. - - The protocol compiler currently uses this metaclass to create protocol - message classes at runtime. Clients can also manually create their own - classes at runtime, as in this example: - - mydescriptor = Descriptor(.....) - factory = symbol_database.Default() - factory.pool.AddDescriptor(mydescriptor) - MyProtoClass = factory.GetPrototype(mydescriptor) - myproto_instance = MyProtoClass() - myproto.foo_field = 23 - ... - """ - - # Must be consistent with the protocol-compiler code in - # proto2/compiler/internal/generator.*. - _DESCRIPTOR_KEY = 'DESCRIPTOR' - - def __new__(cls, name, bases, dictionary): - """Custom allocation for runtime-generated class types. - - We override __new__ because this is apparently the only place - where we can meaningfully set __slots__ on the class we're creating(?). - (The interplay between metaclasses and slots is not very well-documented). - - Args: - name: Name of the class (ignored, but required by the - metaclass protocol). - bases: Base classes of the class we're constructing. - (Should be message.Message). We ignore this field, but - it's required by the metaclass protocol - dictionary: The class dictionary of the class we're - constructing. dictionary[_DESCRIPTOR_KEY] must contain - a Descriptor object describing this protocol message - type. - - Returns: - Newly-allocated class. - - Raises: - RuntimeError: Generated code only work with python cpp extension. - """ - descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] - - if isinstance(descriptor, str): - raise RuntimeError('The generated code only work with python cpp ' - 'extension, but it is using pure python runtime.') - - # If a concrete class already exists for this descriptor, don't try to - # create another. Doing so will break any messages that already exist with - # the existing class. - # - # The C++ implementation appears to have its own internal `PyMessageFactory` - # to achieve similar results. - # - # This most commonly happens in `text_format.py` when using descriptors from - # a custom pool; it calls symbol_database.Global().getPrototype() on a - # descriptor which already has an existing concrete class. - new_class = getattr(descriptor, '_concrete_class', None) - if new_class: - return new_class - - if descriptor.full_name in well_known_types.WKTBASES: - bases += (well_known_types.WKTBASES[descriptor.full_name],) - _AddClassAttributesForNestedExtensions(descriptor, dictionary) - _AddSlots(descriptor, dictionary) - - superclass = super(GeneratedProtocolMessageType, cls) - new_class = superclass.__new__(cls, name, bases, dictionary) - return new_class - - def __init__(cls, name, bases, dictionary): - """Here we perform the majority of our work on the class. - We add enum getters, an __init__ method, implementations - of all Message methods, and properties for all fields - in the protocol type. - - Args: - name: Name of the class (ignored, but required by the - metaclass protocol). - bases: Base classes of the class we're constructing. - (Should be message.Message). We ignore this field, but - it's required by the metaclass protocol - dictionary: The class dictionary of the class we're - constructing. dictionary[_DESCRIPTOR_KEY] must contain - a Descriptor object describing this protocol message - type. - """ - descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] - - # If this is an _existing_ class looked up via `_concrete_class` in the - # __new__ method above, then we don't need to re-initialize anything. - existing_class = getattr(descriptor, '_concrete_class', None) - if existing_class: - assert existing_class is cls, ( - 'Duplicate `GeneratedProtocolMessageType` created for descriptor %r' - % (descriptor.full_name)) - return - - cls._decoders_by_tag = {} - if (descriptor.has_options and - descriptor.GetOptions().message_set_wire_format): - cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = ( - decoder.MessageSetItemDecoder(descriptor), None) - - # Attach stuff to each FieldDescriptor for quick lookup later on. - for field in descriptor.fields: - _AttachFieldHelpers(cls, field) - - descriptor._concrete_class = cls # pylint: disable=protected-access - _AddEnumValues(descriptor, cls) - _AddInitMethod(descriptor, cls) - _AddPropertiesForFields(descriptor, cls) - _AddPropertiesForExtensions(descriptor, cls) - _AddStaticMethods(cls) - _AddMessageMethods(descriptor, cls) - _AddPrivateHelperMethods(descriptor, cls) - - superclass = super(GeneratedProtocolMessageType, cls) - superclass.__init__(name, bases, dictionary) - - -# Stateless helpers for GeneratedProtocolMessageType below. -# Outside clients should not access these directly. -# -# I opted not to make any of these methods on the metaclass, to make it more -# clear that I'm not really using any state there and to keep clients from -# thinking that they have direct access to these construction helpers. - - -def _PropertyName(proto_field_name): - """Returns the name of the public property attribute which - clients can use to get and (in some cases) set the value - of a protocol message field. - - Args: - proto_field_name: The protocol message field name, exactly - as it appears (or would appear) in a .proto file. - """ - # TODO(robinson): Escape Python keywords (e.g., yield), and test this support. - # nnorwitz makes my day by writing: - # """ - # FYI. See the keyword module in the stdlib. This could be as simple as: - # - # if keyword.iskeyword(proto_field_name): - # return proto_field_name + "_" - # return proto_field_name - # """ - # Kenton says: The above is a BAD IDEA. People rely on being able to use - # getattr() and setattr() to reflectively manipulate field values. If we - # rename the properties, then every such user has to also make sure to apply - # the same transformation. Note that currently if you name a field "yield", - # you can still access it just fine using getattr/setattr -- it's not even - # that cumbersome to do so. - # TODO(kenton): Remove this method entirely if/when everyone agrees with my - # position. - return proto_field_name - - -def _AddSlots(message_descriptor, dictionary): - """Adds a __slots__ entry to dictionary, containing the names of all valid - attributes for this message type. - - Args: - message_descriptor: A Descriptor instance describing this message type. - dictionary: Class dictionary to which we'll add a '__slots__' entry. - """ - dictionary['__slots__'] = ['_cached_byte_size', - '_cached_byte_size_dirty', - '_fields', - '_unknown_fields', - '_unknown_field_set', - '_is_present_in_parent', - '_listener', - '_listener_for_children', - '__weakref__', - '_oneofs'] - - -def _IsMessageSetExtension(field): - return (field.is_extension and - field.containing_type.has_options and - field.containing_type.GetOptions().message_set_wire_format and - field.type == _FieldDescriptor.TYPE_MESSAGE and - field.label == _FieldDescriptor.LABEL_OPTIONAL) - - -def _IsMapField(field): - return (field.type == _FieldDescriptor.TYPE_MESSAGE and - field.message_type.has_options and - field.message_type.GetOptions().map_entry) - - -def _IsMessageMapField(field): - value_type = field.message_type.fields_by_name['value'] - return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE - - -def _AttachFieldHelpers(cls, field_descriptor): - is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED) - is_map_entry = _IsMapField(field_descriptor) - is_packed = field_descriptor.is_packed - - if is_map_entry: - field_encoder = encoder.MapEncoder(field_descriptor) - sizer = encoder.MapSizer(field_descriptor, - _IsMessageMapField(field_descriptor)) - elif _IsMessageSetExtension(field_descriptor): - field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number) - sizer = encoder.MessageSetItemSizer(field_descriptor.number) - else: - field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type]( - field_descriptor.number, is_repeated, is_packed) - sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type]( - field_descriptor.number, is_repeated, is_packed) - - field_descriptor._encoder = field_encoder - field_descriptor._sizer = sizer - field_descriptor._default_constructor = _DefaultValueConstructorForField( - field_descriptor) - - def AddDecoder(wiretype, is_packed): - tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype) - decode_type = field_descriptor.type - if (decode_type == _FieldDescriptor.TYPE_ENUM and - not field_descriptor.enum_type.is_closed): - decode_type = _FieldDescriptor.TYPE_INT32 - - oneof_descriptor = None - if field_descriptor.containing_oneof is not None: - oneof_descriptor = field_descriptor - - if is_map_entry: - is_message_map = _IsMessageMapField(field_descriptor) - - field_decoder = decoder.MapDecoder( - field_descriptor, _GetInitializeDefaultForMap(field_descriptor), - is_message_map) - elif decode_type == _FieldDescriptor.TYPE_STRING: - field_decoder = decoder.StringDecoder( - field_descriptor.number, is_repeated, is_packed, - field_descriptor, field_descriptor._default_constructor, - not field_descriptor.has_presence) - elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( - field_descriptor.number, is_repeated, is_packed, - field_descriptor, field_descriptor._default_constructor) - else: - field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( - field_descriptor.number, is_repeated, is_packed, - # pylint: disable=protected-access - field_descriptor, field_descriptor._default_constructor, - not field_descriptor.has_presence) - - cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor) - - AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type], - False) - - if is_repeated and wire_format.IsTypePackable(field_descriptor.type): - # To support wire compatibility of adding packed = true, add a decoder for - # packed values regardless of the field's options. - AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True) - - -def _AddClassAttributesForNestedExtensions(descriptor, dictionary): - extensions = descriptor.extensions_by_name - for extension_name, extension_field in extensions.items(): - assert extension_name not in dictionary - dictionary[extension_name] = extension_field - - -def _AddEnumValues(descriptor, cls): - """Sets class-level attributes for all enum fields defined in this message. - - Also exporting a class-level object that can name enum values. - - Args: - descriptor: Descriptor object for this message type. - cls: Class we're constructing for this message type. - """ - for enum_type in descriptor.enum_types: - setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type)) - for enum_value in enum_type.values: - setattr(cls, enum_value.name, enum_value.number) - - -def _GetInitializeDefaultForMap(field): - if field.label != _FieldDescriptor.LABEL_REPEATED: - raise ValueError('map_entry set on non-repeated field %s' % ( - field.name)) - fields_by_name = field.message_type.fields_by_name - key_checker = type_checkers.GetTypeChecker(fields_by_name['key']) - - value_field = fields_by_name['value'] - if _IsMessageMapField(field): - def MakeMessageMapDefault(message): - return containers.MessageMap( - message._listener_for_children, value_field.message_type, key_checker, - field.message_type) - return MakeMessageMapDefault - else: - value_checker = type_checkers.GetTypeChecker(value_field) - def MakePrimitiveMapDefault(message): - return containers.ScalarMap( - message._listener_for_children, key_checker, value_checker, - field.message_type) - return MakePrimitiveMapDefault - -def _DefaultValueConstructorForField(field): - """Returns a function which returns a default value for a field. - - Args: - field: FieldDescriptor object for this field. - - The returned function has one argument: - message: Message instance containing this field, or a weakref proxy - of same. - - That function in turn returns a default value for this field. The default - value may refer back to |message| via a weak reference. - """ - - if _IsMapField(field): - return _GetInitializeDefaultForMap(field) - - if field.label == _FieldDescriptor.LABEL_REPEATED: - if field.has_default_value and field.default_value != []: - raise ValueError('Repeated field default value not empty list: %s' % ( - field.default_value)) - if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - # We can't look at _concrete_class yet since it might not have - # been set. (Depends on order in which we initialize the classes). - message_type = field.message_type - def MakeRepeatedMessageDefault(message): - return containers.RepeatedCompositeFieldContainer( - message._listener_for_children, field.message_type) - return MakeRepeatedMessageDefault - else: - type_checker = type_checkers.GetTypeChecker(field) - def MakeRepeatedScalarDefault(message): - return containers.RepeatedScalarFieldContainer( - message._listener_for_children, type_checker) - return MakeRepeatedScalarDefault - - if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - # _concrete_class may not yet be initialized. - message_type = field.message_type - def MakeSubMessageDefault(message): - assert getattr(message_type, '_concrete_class', None), ( - 'Uninitialized concrete class found for field %r (message type %r)' - % (field.full_name, message_type.full_name)) - result = message_type._concrete_class() - result._SetListener( - _OneofListener(message, field) - if field.containing_oneof is not None - else message._listener_for_children) - return result - return MakeSubMessageDefault - - def MakeScalarDefault(message): - # TODO(protobuf-team): This may be broken since there may not be - # default_value. Combine with has_default_value somehow. - return field.default_value - return MakeScalarDefault - - -def _ReraiseTypeErrorWithFieldName(message_name, field_name): - """Re-raise the currently-handled TypeError with the field name added.""" - exc = sys.exc_info()[1] - if len(exc.args) == 1 and type(exc) is TypeError: - # simple TypeError; add field name to exception message - exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name)) - - # re-raise possibly-amended exception with original traceback: - raise exc.with_traceback(sys.exc_info()[2]) - - -def _AddInitMethod(message_descriptor, cls): - """Adds an __init__ method to cls.""" - - def _GetIntegerEnumValue(enum_type, value): - """Convert a string or integer enum value to an integer. - - If the value is a string, it is converted to the enum value in - enum_type with the same name. If the value is not a string, it's - returned as-is. (No conversion or bounds-checking is done.) - """ - if isinstance(value, str): - try: - return enum_type.values_by_name[value].number - except KeyError: - raise ValueError('Enum type %s: unknown label "%s"' % ( - enum_type.full_name, value)) - return value - - def init(self, **kwargs): - self._cached_byte_size = 0 - self._cached_byte_size_dirty = len(kwargs) > 0 - self._fields = {} - # Contains a mapping from oneof field descriptors to the descriptor - # of the currently set field in that oneof field. - self._oneofs = {} - - # _unknown_fields is () when empty for efficiency, and will be turned into - # a list if fields are added. - self._unknown_fields = () - # _unknown_field_set is None when empty for efficiency, and will be - # turned into UnknownFieldSet struct if fields are added. - self._unknown_field_set = None # pylint: disable=protected-access - self._is_present_in_parent = False - self._listener = message_listener_mod.NullMessageListener() - self._listener_for_children = _Listener(self) - for field_name, field_value in kwargs.items(): - field = _GetFieldByName(message_descriptor, field_name) - if field is None: - raise TypeError('%s() got an unexpected keyword argument "%s"' % - (message_descriptor.name, field_name)) - if field_value is None: - # field=None is the same as no field at all. - continue - if field.label == _FieldDescriptor.LABEL_REPEATED: - copy = field._default_constructor(self) - if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # Composite - if _IsMapField(field): - if _IsMessageMapField(field): - for key in field_value: - copy[key].MergeFrom(field_value[key]) - else: - copy.update(field_value) - else: - for val in field_value: - if isinstance(val, dict): - copy.add(**val) - else: - copy.add().MergeFrom(val) - else: # Scalar - if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: - field_value = [_GetIntegerEnumValue(field.enum_type, val) - for val in field_value] - copy.extend(field_value) - self._fields[field] = copy - elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - copy = field._default_constructor(self) - new_val = field_value - if isinstance(field_value, dict): - new_val = field.message_type._concrete_class(**field_value) - try: - copy.MergeFrom(new_val) - except TypeError: - _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) - self._fields[field] = copy - else: - if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: - field_value = _GetIntegerEnumValue(field.enum_type, field_value) - try: - setattr(self, field_name, field_value) - except TypeError: - _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) - - init.__module__ = None - init.__doc__ = None - cls.__init__ = init - - -def _GetFieldByName(message_descriptor, field_name): - """Returns a field descriptor by field name. - - Args: - message_descriptor: A Descriptor describing all fields in message. - field_name: The name of the field to retrieve. - Returns: - The field descriptor associated with the field name. - """ - try: - return message_descriptor.fields_by_name[field_name] - except KeyError: - raise ValueError('Protocol message %s has no "%s" field.' % - (message_descriptor.name, field_name)) - - -def _AddPropertiesForFields(descriptor, cls): - """Adds properties for all fields in this protocol message type.""" - for field in descriptor.fields: - _AddPropertiesForField(field, cls) - - if descriptor.is_extendable: - # _ExtensionDict is just an adaptor with no state so we allocate a new one - # every time it is accessed. - cls.Extensions = property(lambda self: _ExtensionDict(self)) - - -def _AddPropertiesForField(field, cls): - """Adds a public property for a protocol message field. - Clients can use this property to get and (in the case - of non-repeated scalar fields) directly set the value - of a protocol message field. - - Args: - field: A FieldDescriptor for this field. - cls: The class we're constructing. - """ - # Catch it if we add other types that we should - # handle specially here. - assert _FieldDescriptor.MAX_CPPTYPE == 10 - - constant_name = field.name.upper() + '_FIELD_NUMBER' - setattr(cls, constant_name, field.number) - - if field.label == _FieldDescriptor.LABEL_REPEATED: - _AddPropertiesForRepeatedField(field, cls) - elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - _AddPropertiesForNonRepeatedCompositeField(field, cls) - else: - _AddPropertiesForNonRepeatedScalarField(field, cls) - - -class _FieldProperty(property): - __slots__ = ('DESCRIPTOR',) - - def __init__(self, descriptor, getter, setter, doc): - property.__init__(self, getter, setter, doc=doc) - self.DESCRIPTOR = descriptor - - -def _AddPropertiesForRepeatedField(field, cls): - """Adds a public property for a "repeated" protocol message field. Clients - can use this property to get the value of the field, which will be either a - RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see - below). - - Note that when clients add values to these containers, we perform - type-checking in the case of repeated scalar fields, and we also set any - necessary "has" bits as a side-effect. - - Args: - field: A FieldDescriptor for this field. - cls: The class we're constructing. - """ - proto_field_name = field.name - property_name = _PropertyName(proto_field_name) - - def getter(self): - field_value = self._fields.get(field) - if field_value is None: - # Construct a new object to represent this field. - field_value = field._default_constructor(self) - - # Atomically check if another thread has preempted us and, if not, swap - # in the new object we just created. If someone has preempted us, we - # take that object and discard ours. - # WARNING: We are relying on setdefault() being atomic. This is true - # in CPython but we haven't investigated others. This warning appears - # in several other locations in this file. - field_value = self._fields.setdefault(field, field_value) - return field_value - getter.__module__ = None - getter.__doc__ = 'Getter for %s.' % proto_field_name - - # We define a setter just so we can throw an exception with a more - # helpful error message. - def setter(self, new_value): - raise AttributeError('Assignment not allowed to repeated field ' - '"%s" in protocol message object.' % proto_field_name) - - doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name - setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) - - -def _AddPropertiesForNonRepeatedScalarField(field, cls): - """Adds a public property for a nonrepeated, scalar protocol message field. - Clients can use this property to get and directly set the value of the field. - Note that when the client sets the value of a field by using this property, - all necessary "has" bits are set as a side-effect, and we also perform - type-checking. - - Args: - field: A FieldDescriptor for this field. - cls: The class we're constructing. - """ - proto_field_name = field.name - property_name = _PropertyName(proto_field_name) - type_checker = type_checkers.GetTypeChecker(field) - default_value = field.default_value - - def getter(self): - # TODO(protobuf-team): This may be broken since there may not be - # default_value. Combine with has_default_value somehow. - return self._fields.get(field, default_value) - getter.__module__ = None - getter.__doc__ = 'Getter for %s.' % proto_field_name - - def field_setter(self, new_value): - # pylint: disable=protected-access - # Testing the value for truthiness captures all of the proto3 defaults - # (0, 0.0, enum 0, and False). - try: - new_value = type_checker.CheckValue(new_value) - except TypeError as e: - raise TypeError( - 'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e)) - if not field.has_presence and not new_value: - self._fields.pop(field, None) - else: - self._fields[field] = new_value - # Check _cached_byte_size_dirty inline to improve performance, since scalar - # setters are called frequently. - if not self._cached_byte_size_dirty: - self._Modified() - - if field.containing_oneof: - def setter(self, new_value): - field_setter(self, new_value) - self._UpdateOneofState(field) - else: - setter = field_setter - - setter.__module__ = None - setter.__doc__ = 'Setter for %s.' % proto_field_name - - # Add a property to encapsulate the getter/setter. - doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name - setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) - - -def _AddPropertiesForNonRepeatedCompositeField(field, cls): - """Adds a public property for a nonrepeated, composite protocol message field. - A composite field is a "group" or "message" field. - - Clients can use this property to get the value of the field, but cannot - assign to the property directly. - - Args: - field: A FieldDescriptor for this field. - cls: The class we're constructing. - """ - # TODO(robinson): Remove duplication with similar method - # for non-repeated scalars. - proto_field_name = field.name - property_name = _PropertyName(proto_field_name) - - def getter(self): - field_value = self._fields.get(field) - if field_value is None: - # Construct a new object to represent this field. - field_value = field._default_constructor(self) - - # Atomically check if another thread has preempted us and, if not, swap - # in the new object we just created. If someone has preempted us, we - # take that object and discard ours. - # WARNING: We are relying on setdefault() being atomic. This is true - # in CPython but we haven't investigated others. This warning appears - # in several other locations in this file. - field_value = self._fields.setdefault(field, field_value) - return field_value - getter.__module__ = None - getter.__doc__ = 'Getter for %s.' % proto_field_name - - # We define a setter just so we can throw an exception with a more - # helpful error message. - def setter(self, new_value): - raise AttributeError('Assignment not allowed to composite field ' - '"%s" in protocol message object.' % proto_field_name) - - # Add a property to encapsulate the getter. - doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name - setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) - - -def _AddPropertiesForExtensions(descriptor, cls): - """Adds properties for all fields in this protocol message type.""" - extensions = descriptor.extensions_by_name - for extension_name, extension_field in extensions.items(): - constant_name = extension_name.upper() + '_FIELD_NUMBER' - setattr(cls, constant_name, extension_field.number) - - # TODO(amauryfa): Migrate all users of these attributes to functions like - # pool.FindExtensionByNumber(descriptor). - if descriptor.file is not None: - # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. - pool = descriptor.file.pool - cls._extensions_by_number = pool._extensions_by_number[descriptor] - cls._extensions_by_name = pool._extensions_by_name[descriptor] - -def _AddStaticMethods(cls): - # TODO(robinson): This probably needs to be thread-safe(?) - def RegisterExtension(field_descriptor): - field_descriptor.containing_type = cls.DESCRIPTOR - # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. - # pylint: disable=protected-access - cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(field_descriptor) - _AttachFieldHelpers(cls, field_descriptor) - cls.RegisterExtension = staticmethod(RegisterExtension) - - def FromString(s): - message = cls() - message.MergeFromString(s) - return message - cls.FromString = staticmethod(FromString) - - -def _IsPresent(item): - """Given a (FieldDescriptor, value) tuple from _fields, return true if the - value should be included in the list returned by ListFields().""" - - if item[0].label == _FieldDescriptor.LABEL_REPEATED: - return bool(item[1]) - elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - return item[1]._is_present_in_parent - else: - return True - - -def _AddListFieldsMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - - def ListFields(self): - all_fields = [item for item in self._fields.items() if _IsPresent(item)] - all_fields.sort(key = lambda item: item[0].number) - return all_fields - - cls.ListFields = ListFields - - -def _AddHasFieldMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - - hassable_fields = {} - for field in message_descriptor.fields: - if field.label == _FieldDescriptor.LABEL_REPEATED: - continue - # For proto3, only submessages and fields inside a oneof have presence. - if not field.has_presence: - continue - hassable_fields[field.name] = field - - # Has methods are supported for oneof descriptors. - for oneof in message_descriptor.oneofs: - hassable_fields[oneof.name] = oneof - - def HasField(self, field_name): - try: - field = hassable_fields[field_name] - except KeyError as exc: - raise ValueError('Protocol message %s has no non-repeated field "%s" ' - 'nor has presence is not available for this field.' % ( - message_descriptor.full_name, field_name)) from exc - - if isinstance(field, descriptor_mod.OneofDescriptor): - try: - return HasField(self, self._oneofs[field].name) - except KeyError: - return False - else: - if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - value = self._fields.get(field) - return value is not None and value._is_present_in_parent - else: - return field in self._fields - - cls.HasField = HasField - - -def _AddClearFieldMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - def ClearField(self, field_name): - try: - field = message_descriptor.fields_by_name[field_name] - except KeyError: - try: - field = message_descriptor.oneofs_by_name[field_name] - if field in self._oneofs: - field = self._oneofs[field] - else: - return - except KeyError: - raise ValueError('Protocol message %s has no "%s" field.' % - (message_descriptor.name, field_name)) - - if field in self._fields: - # To match the C++ implementation, we need to invalidate iterators - # for map fields when ClearField() happens. - if hasattr(self._fields[field], 'InvalidateIterators'): - self._fields[field].InvalidateIterators() - - # Note: If the field is a sub-message, its listener will still point - # at us. That's fine, because the worst than can happen is that it - # will call _Modified() and invalidate our byte size. Big deal. - del self._fields[field] - - if self._oneofs.get(field.containing_oneof, None) is field: - del self._oneofs[field.containing_oneof] - - # Always call _Modified() -- even if nothing was changed, this is - # a mutating method, and thus calling it should cause the field to become - # present in the parent message. - self._Modified() - - cls.ClearField = ClearField - - -def _AddClearExtensionMethod(cls): - """Helper for _AddMessageMethods().""" - def ClearExtension(self, field_descriptor): - extension_dict._VerifyExtensionHandle(self, field_descriptor) - - # Similar to ClearField(), above. - if field_descriptor in self._fields: - del self._fields[field_descriptor] - self._Modified() - cls.ClearExtension = ClearExtension - - -def _AddHasExtensionMethod(cls): - """Helper for _AddMessageMethods().""" - def HasExtension(self, field_descriptor): - extension_dict._VerifyExtensionHandle(self, field_descriptor) - if field_descriptor.label == _FieldDescriptor.LABEL_REPEATED: - raise KeyError('"%s" is repeated.' % field_descriptor.full_name) - - if field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - value = self._fields.get(field_descriptor) - return value is not None and value._is_present_in_parent - else: - return field_descriptor in self._fields - cls.HasExtension = HasExtension - -def _InternalUnpackAny(msg): - """Unpacks Any message and returns the unpacked message. - - This internal method is different from public Any Unpack method which takes - the target message as argument. _InternalUnpackAny method does not have - target message type and need to find the message type in descriptor pool. - - Args: - msg: An Any message to be unpacked. - - Returns: - The unpacked message. - """ - # TODO(amauryfa): Don't use the factory of generated messages. - # To make Any work with custom factories, use the message factory of the - # parent message. - # pylint: disable=g-import-not-at-top - from google.protobuf import symbol_database - factory = symbol_database.Default() - - type_url = msg.type_url - - if not type_url: - return None - - # TODO(haberman): For now we just strip the hostname. Better logic will be - # required. - type_name = type_url.split('/')[-1] - descriptor = factory.pool.FindMessageTypeByName(type_name) - - if descriptor is None: - return None - - message_class = factory.GetPrototype(descriptor) - message = message_class() - - message.ParseFromString(msg.value) - return message - - -def _AddEqualsMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - def __eq__(self, other): - if (not isinstance(other, message_mod.Message) or - other.DESCRIPTOR != self.DESCRIPTOR): - return False - - if self is other: - return True - - if self.DESCRIPTOR.full_name == _AnyFullTypeName: - any_a = _InternalUnpackAny(self) - any_b = _InternalUnpackAny(other) - if any_a and any_b: - return any_a == any_b - - if not self.ListFields() == other.ListFields(): - return False - - # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions, - # then use it for the comparison. - unknown_fields = list(self._unknown_fields) - unknown_fields.sort() - other_unknown_fields = list(other._unknown_fields) - other_unknown_fields.sort() - return unknown_fields == other_unknown_fields - - cls.__eq__ = __eq__ - - -def _AddStrMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - def __str__(self): - return text_format.MessageToString(self) - cls.__str__ = __str__ - - -def _AddReprMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - def __repr__(self): - return text_format.MessageToString(self) - cls.__repr__ = __repr__ - - -def _AddUnicodeMethod(unused_message_descriptor, cls): - """Helper for _AddMessageMethods().""" - - def __unicode__(self): - return text_format.MessageToString(self, as_utf8=True).decode('utf-8') - cls.__unicode__ = __unicode__ - - -def _BytesForNonRepeatedElement(value, field_number, field_type): - """Returns the number of bytes needed to serialize a non-repeated element. - The returned byte count includes space for tag information and any - other additional space associated with serializing value. - - Args: - value: Value we're serializing. - field_number: Field number of this value. (Since the field number - is stored as part of a varint-encoded tag, this has an impact - on the total bytes required to serialize the value). - field_type: The type of the field. One of the TYPE_* constants - within FieldDescriptor. - """ - try: - fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type] - return fn(field_number, value) - except KeyError: - raise message_mod.EncodeError('Unrecognized field type: %d' % field_type) - - -def _AddByteSizeMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - - def ByteSize(self): - if not self._cached_byte_size_dirty: - return self._cached_byte_size - - size = 0 - descriptor = self.DESCRIPTOR - if descriptor.GetOptions().map_entry: - # Fields of map entry should always be serialized. - size = descriptor.fields_by_name['key']._sizer(self.key) - size += descriptor.fields_by_name['value']._sizer(self.value) - else: - for field_descriptor, field_value in self.ListFields(): - size += field_descriptor._sizer(field_value) - for tag_bytes, value_bytes in self._unknown_fields: - size += len(tag_bytes) + len(value_bytes) - - self._cached_byte_size = size - self._cached_byte_size_dirty = False - self._listener_for_children.dirty = False - return size - - cls.ByteSize = ByteSize - - -def _AddSerializeToStringMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - - def SerializeToString(self, **kwargs): - # Check if the message has all of its required fields set. - if not self.IsInitialized(): - raise message_mod.EncodeError( - 'Message %s is missing required fields: %s' % ( - self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors()))) - return self.SerializePartialToString(**kwargs) - cls.SerializeToString = SerializeToString - - -def _AddSerializePartialToStringMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - - def SerializePartialToString(self, **kwargs): - out = BytesIO() - self._InternalSerialize(out.write, **kwargs) - return out.getvalue() - cls.SerializePartialToString = SerializePartialToString - - def InternalSerialize(self, write_bytes, deterministic=None): - if deterministic is None: - deterministic = ( - api_implementation.IsPythonDefaultSerializationDeterministic()) - else: - deterministic = bool(deterministic) - - descriptor = self.DESCRIPTOR - if descriptor.GetOptions().map_entry: - # Fields of map entry should always be serialized. - descriptor.fields_by_name['key']._encoder( - write_bytes, self.key, deterministic) - descriptor.fields_by_name['value']._encoder( - write_bytes, self.value, deterministic) - else: - for field_descriptor, field_value in self.ListFields(): - field_descriptor._encoder(write_bytes, field_value, deterministic) - for tag_bytes, value_bytes in self._unknown_fields: - write_bytes(tag_bytes) - write_bytes(value_bytes) - cls._InternalSerialize = InternalSerialize - - -def _AddMergeFromStringMethod(message_descriptor, cls): - """Helper for _AddMessageMethods().""" - def MergeFromString(self, serialized): - serialized = memoryview(serialized) - length = len(serialized) - try: - if self._InternalParse(serialized, 0, length) != length: - # The only reason _InternalParse would return early is if it - # encountered an end-group tag. - raise message_mod.DecodeError('Unexpected end-group tag.') - except (IndexError, TypeError): - # Now ord(buf[p:p+1]) == ord('') gets TypeError. - raise message_mod.DecodeError('Truncated message.') - except struct.error as e: - raise message_mod.DecodeError(e) - return length # Return this for legacy reasons. - cls.MergeFromString = MergeFromString - - local_ReadTag = decoder.ReadTag - local_SkipField = decoder.SkipField - decoders_by_tag = cls._decoders_by_tag - - def InternalParse(self, buffer, pos, end): - """Create a message from serialized bytes. - - Args: - self: Message, instance of the proto message object. - buffer: memoryview of the serialized data. - pos: int, position to start in the serialized data. - end: int, end position of the serialized data. - - Returns: - Message object. - """ - # Guard against internal misuse, since this function is called internally - # quite extensively, and its easy to accidentally pass bytes. - assert isinstance(buffer, memoryview) - self._Modified() - field_dict = self._fields - # pylint: disable=protected-access - unknown_field_set = self._unknown_field_set - while pos != end: - (tag_bytes, new_pos) = local_ReadTag(buffer, pos) - field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None)) - if field_decoder is None: - if not self._unknown_fields: # pylint: disable=protected-access - self._unknown_fields = [] # pylint: disable=protected-access - if unknown_field_set is None: - # pylint: disable=protected-access - self._unknown_field_set = containers.UnknownFieldSet() - # pylint: disable=protected-access - unknown_field_set = self._unknown_field_set - # pylint: disable=protected-access - (tag, _) = decoder._DecodeVarint(tag_bytes, 0) - field_number, wire_type = wire_format.UnpackTag(tag) - if field_number == 0: - raise message_mod.DecodeError('Field number 0 is illegal.') - # TODO(jieluo): remove old_pos. - old_pos = new_pos - (data, new_pos) = decoder._DecodeUnknownField( - buffer, new_pos, wire_type) # pylint: disable=protected-access - if new_pos == -1: - return pos - # pylint: disable=protected-access - unknown_field_set._add(field_number, wire_type, data) - # TODO(jieluo): remove _unknown_fields. - new_pos = local_SkipField(buffer, old_pos, end, tag_bytes) - if new_pos == -1: - return pos - self._unknown_fields.append( - (tag_bytes, buffer[old_pos:new_pos].tobytes())) - pos = new_pos - else: - pos = field_decoder(buffer, new_pos, end, self, field_dict) - if field_desc: - self._UpdateOneofState(field_desc) - return pos - cls._InternalParse = InternalParse - - -def _AddIsInitializedMethod(message_descriptor, cls): - """Adds the IsInitialized and FindInitializationError methods to the - protocol message class.""" - - required_fields = [field for field in message_descriptor.fields - if field.label == _FieldDescriptor.LABEL_REQUIRED] - - def IsInitialized(self, errors=None): - """Checks if all required fields of a message are set. - - Args: - errors: A list which, if provided, will be populated with the field - paths of all missing required fields. - - Returns: - True iff the specified message has all required fields set. - """ - - # Performance is critical so we avoid HasField() and ListFields(). - - for field in required_fields: - if (field not in self._fields or - (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and - not self._fields[field]._is_present_in_parent)): - if errors is not None: - errors.extend(self.FindInitializationErrors()) - return False - - for field, value in list(self._fields.items()): # dict can change size! - if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - if field.label == _FieldDescriptor.LABEL_REPEATED: - if (field.message_type.has_options and - field.message_type.GetOptions().map_entry): - continue - for element in value: - if not element.IsInitialized(): - if errors is not None: - errors.extend(self.FindInitializationErrors()) - return False - elif value._is_present_in_parent and not value.IsInitialized(): - if errors is not None: - errors.extend(self.FindInitializationErrors()) - return False - - return True - - cls.IsInitialized = IsInitialized - - def FindInitializationErrors(self): - """Finds required fields which are not initialized. - - Returns: - A list of strings. Each string is a path to an uninitialized field from - the top-level message, e.g. "foo.bar[5].baz". - """ - - errors = [] # simplify things - - for field in required_fields: - if not self.HasField(field.name): - errors.append(field.name) - - for field, value in self.ListFields(): - if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - if field.is_extension: - name = '(%s)' % field.full_name - else: - name = field.name - - if _IsMapField(field): - if _IsMessageMapField(field): - for key in value: - element = value[key] - prefix = '%s[%s].' % (name, key) - sub_errors = element.FindInitializationErrors() - errors += [prefix + error for error in sub_errors] - else: - # ScalarMaps can't have any initialization errors. - pass - elif field.label == _FieldDescriptor.LABEL_REPEATED: - for i in range(len(value)): - element = value[i] - prefix = '%s[%d].' % (name, i) - sub_errors = element.FindInitializationErrors() - errors += [prefix + error for error in sub_errors] - else: - prefix = name + '.' - sub_errors = value.FindInitializationErrors() - errors += [prefix + error for error in sub_errors] - - return errors - - cls.FindInitializationErrors = FindInitializationErrors - - -def _FullyQualifiedClassName(klass): - module = klass.__module__ - name = getattr(klass, '__qualname__', klass.__name__) - if module in (None, 'builtins', '__builtin__'): - return name - return module + '.' + name - - -def _AddMergeFromMethod(cls): - LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED - CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE - - def MergeFrom(self, msg): - if not isinstance(msg, cls): - raise TypeError( - 'Parameter to MergeFrom() must be instance of same class: ' - 'expected %s got %s.' % (_FullyQualifiedClassName(cls), - _FullyQualifiedClassName(msg.__class__))) - - assert msg is not self - self._Modified() - - fields = self._fields - - for field, value in msg._fields.items(): - if field.label == LABEL_REPEATED: - field_value = fields.get(field) - if field_value is None: - # Construct a new object to represent this field. - field_value = field._default_constructor(self) - fields[field] = field_value - field_value.MergeFrom(value) - elif field.cpp_type == CPPTYPE_MESSAGE: - if value._is_present_in_parent: - field_value = fields.get(field) - if field_value is None: - # Construct a new object to represent this field. - field_value = field._default_constructor(self) - fields[field] = field_value - field_value.MergeFrom(value) - else: - self._fields[field] = value - if field.containing_oneof: - self._UpdateOneofState(field) - - if msg._unknown_fields: - if not self._unknown_fields: - self._unknown_fields = [] - self._unknown_fields.extend(msg._unknown_fields) - # pylint: disable=protected-access - if self._unknown_field_set is None: - self._unknown_field_set = containers.UnknownFieldSet() - self._unknown_field_set._extend(msg._unknown_field_set) - - cls.MergeFrom = MergeFrom - - -def _AddWhichOneofMethod(message_descriptor, cls): - def WhichOneof(self, oneof_name): - """Returns the name of the currently set field inside a oneof, or None.""" - try: - field = message_descriptor.oneofs_by_name[oneof_name] - except KeyError: - raise ValueError( - 'Protocol message has no oneof "%s" field.' % oneof_name) - - nested_field = self._oneofs.get(field, None) - if nested_field is not None and self.HasField(nested_field.name): - return nested_field.name - else: - return None - - cls.WhichOneof = WhichOneof - - -def _Clear(self): - # Clear fields. - self._fields = {} - self._unknown_fields = () - # pylint: disable=protected-access - if self._unknown_field_set is not None: - self._unknown_field_set._clear() - self._unknown_field_set = None - - self._oneofs = {} - self._Modified() - - -def _UnknownFields(self): - if self._unknown_field_set is None: # pylint: disable=protected-access - # pylint: disable=protected-access - self._unknown_field_set = containers.UnknownFieldSet() - return self._unknown_field_set # pylint: disable=protected-access - - -def _DiscardUnknownFields(self): - self._unknown_fields = [] - self._unknown_field_set = None # pylint: disable=protected-access - for field, value in self.ListFields(): - if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: - if _IsMapField(field): - if _IsMessageMapField(field): - for key in value: - value[key].DiscardUnknownFields() - elif field.label == _FieldDescriptor.LABEL_REPEATED: - for sub_message in value: - sub_message.DiscardUnknownFields() - else: - value.DiscardUnknownFields() - - -def _SetListener(self, listener): - if listener is None: - self._listener = message_listener_mod.NullMessageListener() - else: - self._listener = listener - - -def _AddMessageMethods(message_descriptor, cls): - """Adds implementations of all Message methods to cls.""" - _AddListFieldsMethod(message_descriptor, cls) - _AddHasFieldMethod(message_descriptor, cls) - _AddClearFieldMethod(message_descriptor, cls) - if message_descriptor.is_extendable: - _AddClearExtensionMethod(cls) - _AddHasExtensionMethod(cls) - _AddEqualsMethod(message_descriptor, cls) - _AddStrMethod(message_descriptor, cls) - _AddReprMethod(message_descriptor, cls) - _AddUnicodeMethod(message_descriptor, cls) - _AddByteSizeMethod(message_descriptor, cls) - _AddSerializeToStringMethod(message_descriptor, cls) - _AddSerializePartialToStringMethod(message_descriptor, cls) - _AddMergeFromStringMethod(message_descriptor, cls) - _AddIsInitializedMethod(message_descriptor, cls) - _AddMergeFromMethod(cls) - _AddWhichOneofMethod(message_descriptor, cls) - # Adds methods which do not depend on cls. - cls.Clear = _Clear - cls.UnknownFields = _UnknownFields - cls.DiscardUnknownFields = _DiscardUnknownFields - cls._SetListener = _SetListener - - -def _AddPrivateHelperMethods(message_descriptor, cls): - """Adds implementation of private helper methods to cls.""" - - def Modified(self): - """Sets the _cached_byte_size_dirty bit to true, - and propagates this to our listener iff this was a state change. - """ - - # Note: Some callers check _cached_byte_size_dirty before calling - # _Modified() as an extra optimization. So, if this method is ever - # changed such that it does stuff even when _cached_byte_size_dirty is - # already true, the callers need to be updated. - if not self._cached_byte_size_dirty: - self._cached_byte_size_dirty = True - self._listener_for_children.dirty = True - self._is_present_in_parent = True - self._listener.Modified() - - def _UpdateOneofState(self, field): - """Sets field as the active field in its containing oneof. - - Will also delete currently active field in the oneof, if it is different - from the argument. Does not mark the message as modified. - """ - other_field = self._oneofs.setdefault(field.containing_oneof, field) - if other_field is not field: - del self._fields[other_field] - self._oneofs[field.containing_oneof] = field - - cls._Modified = Modified - cls.SetInParent = Modified - cls._UpdateOneofState = _UpdateOneofState - - -class _Listener(object): - - """MessageListener implementation that a parent message registers with its - child message. - - In order to support semantics like: - - foo.bar.baz.moo = 23 - assert foo.HasField('bar') - - ...child objects must have back references to their parents. - This helper class is at the heart of this support. - """ - - def __init__(self, parent_message): - """Args: - parent_message: The message whose _Modified() method we should call when - we receive Modified() messages. - """ - # This listener establishes a back reference from a child (contained) object - # to its parent (containing) object. We make this a weak reference to avoid - # creating cyclic garbage when the client finishes with the 'parent' object - # in the tree. - if isinstance(parent_message, weakref.ProxyType): - self._parent_message_weakref = parent_message - else: - self._parent_message_weakref = weakref.proxy(parent_message) - - # As an optimization, we also indicate directly on the listener whether - # or not the parent message is dirty. This way we can avoid traversing - # up the tree in the common case. - self.dirty = False - - def Modified(self): - if self.dirty: - return - try: - # Propagate the signal to our parents iff this is the first field set. - self._parent_message_weakref._Modified() - except ReferenceError: - # We can get here if a client has kept a reference to a child object, - # and is now setting a field on it, but the child's parent has been - # garbage-collected. This is not an error. - pass - - -class _OneofListener(_Listener): - """Special listener implementation for setting composite oneof fields.""" - - def __init__(self, parent_message, field): - """Args: - parent_message: The message whose _Modified() method we should call when - we receive Modified() messages. - field: The descriptor of the field being set in the parent message. - """ - super(_OneofListener, self).__init__(parent_message) - self._field = field - - def Modified(self): - """Also updates the state of the containing oneof in the parent message.""" - try: - self._parent_message_weakref._UpdateOneofState(self._field) - super(_OneofListener, self).Modified() - except ReferenceError: - pass diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/testing_refleaks.py b/script.module.protobuf/resources/lib/google/protobuf/internal/testing_refleaks.py deleted file mode 100644 index 5f19c46fd..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/testing_refleaks.py +++ /dev/null @@ -1,142 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""A subclass of unittest.TestCase which checks for reference leaks. - -To use: -- Use testing_refleak.BaseTestCase instead of unittest.TestCase -- Configure and compile Python with --with-pydebug - -If sys.gettotalrefcount() is not available (because Python was built without -the Py_DEBUG option), then this module is a no-op and tests will run normally. -""" - -import copyreg -import gc -import sys -import unittest - - -class LocalTestResult(unittest.TestResult): - """A TestResult which forwards events to a parent object, except for Skips.""" - - def __init__(self, parent_result): - unittest.TestResult.__init__(self) - self.parent_result = parent_result - - def addError(self, test, error): - self.parent_result.addError(test, error) - - def addFailure(self, test, error): - self.parent_result.addFailure(test, error) - - def addSkip(self, test, reason): - pass - - -class ReferenceLeakCheckerMixin(object): - """A mixin class for TestCase, which checks reference counts.""" - - NB_RUNS = 3 - - def run(self, result=None): - testMethod = getattr(self, self._testMethodName) - expecting_failure_method = getattr(testMethod, "__unittest_expecting_failure__", False) - expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False) - if expecting_failure_class or expecting_failure_method: - return - - # python_message.py registers all Message classes to some pickle global - # registry, which makes the classes immortal. - # We save a copy of this registry, and reset it before we could references. - self._saved_pickle_registry = copyreg.dispatch_table.copy() - - # Run the test twice, to warm up the instance attributes. - super(ReferenceLeakCheckerMixin, self).run(result=result) - super(ReferenceLeakCheckerMixin, self).run(result=result) - - oldrefcount = 0 - local_result = LocalTestResult(result) - num_flakes = 0 - - refcount_deltas = [] - while len(refcount_deltas) < self.NB_RUNS: - oldrefcount = self._getRefcounts() - super(ReferenceLeakCheckerMixin, self).run(result=local_result) - newrefcount = self._getRefcounts() - # If the GC was able to collect some objects after the call to run() that - # it could not collect before the call, then the counts won't match. - if newrefcount < oldrefcount and num_flakes < 2: - # This result is (probably) a flake -- garbage collectors aren't very - # predictable, but a lower ending refcount is the opposite of the - # failure we are testing for. If the result is repeatable, then we will - # eventually report it, but not after trying to eliminate it. - num_flakes += 1 - continue - num_flakes = 0 - refcount_deltas.append(newrefcount - oldrefcount) - print(refcount_deltas, self) - - try: - self.assertEqual(refcount_deltas, [0] * self.NB_RUNS) - except Exception: # pylint: disable=broad-except - result.addError(self, sys.exc_info()) - - def _getRefcounts(self): - copyreg.dispatch_table.clear() - copyreg.dispatch_table.update(self._saved_pickle_registry) - # It is sometimes necessary to gc.collect() multiple times, to ensure - # that all objects can be collected. - gc.collect() - gc.collect() - gc.collect() - return sys.gettotalrefcount() - - -if hasattr(sys, 'gettotalrefcount'): - - def TestCase(test_class): - new_bases = (ReferenceLeakCheckerMixin,) + test_class.__bases__ - new_class = type(test_class)( - test_class.__name__, new_bases, dict(test_class.__dict__)) - return new_class - SkipReferenceLeakChecker = unittest.skip - -else: - # When PyDEBUG is not enabled, run the tests normally. - - def TestCase(test_class): - return test_class - - def SkipReferenceLeakChecker(reason): - del reason # Don't skip, so don't need a reason. - def Same(func): - return func - return Same diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/type_checkers.py b/script.module.protobuf/resources/lib/google/protobuf/internal/type_checkers.py deleted file mode 100644 index 165dcd8c2..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/type_checkers.py +++ /dev/null @@ -1,431 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Provides type checking routines. - -This module defines type checking utilities in the forms of dictionaries: - -VALUE_CHECKERS: A dictionary of field types and a value validation object. -TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing - function. -TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization - function. -FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their - corresponding wire types. -TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization - function. -""" - -__author__ = 'robinson@google.com (Will Robinson)' - -import ctypes -import numbers - -from google.protobuf.internal import decoder -from google.protobuf.internal import encoder -from google.protobuf.internal import wire_format -from google.protobuf import descriptor - -_FieldDescriptor = descriptor.FieldDescriptor - - -def TruncateToFourByteFloat(original): - return ctypes.c_float(original).value - - -def ToShortestFloat(original): - """Returns the shortest float that has same value in wire.""" - # All 4 byte floats have between 6 and 9 significant digits, so we - # start with 6 as the lower bound. - # It has to be iterative because use '.9g' directly can not get rid - # of the noises for most values. For example if set a float_field=0.9 - # use '.9g' will print 0.899999976. - precision = 6 - rounded = float('{0:.{1}g}'.format(original, precision)) - while TruncateToFourByteFloat(rounded) != original: - precision += 1 - rounded = float('{0:.{1}g}'.format(original, precision)) - return rounded - - -def GetTypeChecker(field): - """Returns a type checker for a message field of the specified types. - - Args: - field: FieldDescriptor object for this field. - - Returns: - An instance of TypeChecker which can be used to verify the types - of values assigned to a field of the specified type. - """ - if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and - field.type == _FieldDescriptor.TYPE_STRING): - return UnicodeValueChecker() - if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: - if field.enum_type.is_closed: - return EnumValueChecker(field.enum_type) - else: - # When open enums are supported, any int32 can be assigned. - return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32] - return _VALUE_CHECKERS[field.cpp_type] - - -# None of the typecheckers below make any attempt to guard against people -# subclassing builtin types and doing weird things. We're not trying to -# protect against malicious clients here, just people accidentally shooting -# themselves in the foot in obvious ways. -class TypeChecker(object): - - """Type checker used to catch type errors as early as possible - when the client is setting scalar fields in protocol messages. - """ - - def __init__(self, *acceptable_types): - self._acceptable_types = acceptable_types - - def CheckValue(self, proposed_value): - """Type check the provided value and return it. - - The returned value might have been normalized to another type. - """ - if not isinstance(proposed_value, self._acceptable_types): - message = ('%.1024r has type %s, but expected one of: %s' % - (proposed_value, type(proposed_value), self._acceptable_types)) - raise TypeError(message) - return proposed_value - - -class TypeCheckerWithDefault(TypeChecker): - - def __init__(self, default_value, *acceptable_types): - TypeChecker.__init__(self, *acceptable_types) - self._default_value = default_value - - def DefaultValue(self): - return self._default_value - - -class BoolValueChecker(object): - """Type checker used for bool fields.""" - - def CheckValue(self, proposed_value): - if not hasattr(proposed_value, '__index__') or ( - type(proposed_value).__module__ == 'numpy' and - type(proposed_value).__name__ == 'ndarray'): - message = ('%.1024r has type %s, but expected one of: %s' % - (proposed_value, type(proposed_value), (bool, int))) - raise TypeError(message) - return bool(proposed_value) - - def DefaultValue(self): - return False - - -# IntValueChecker and its subclasses perform integer type-checks -# and bounds-checks. -class IntValueChecker(object): - - """Checker used for integer fields. Performs type-check and range check.""" - - def CheckValue(self, proposed_value): - if not hasattr(proposed_value, '__index__') or ( - type(proposed_value).__module__ == 'numpy' and - type(proposed_value).__name__ == 'ndarray'): - message = ('%.1024r has type %s, but expected one of: %s' % - (proposed_value, type(proposed_value), (int,))) - raise TypeError(message) - - if not self._MIN <= int(proposed_value) <= self._MAX: - raise ValueError('Value out of range: %d' % proposed_value) - # We force all values to int to make alternate implementations where the - # distinction is more significant (e.g. the C++ implementation) simpler. - proposed_value = int(proposed_value) - return proposed_value - - def DefaultValue(self): - return 0 - - -class EnumValueChecker(object): - - """Checker used for enum fields. Performs type-check and range check.""" - - def __init__(self, enum_type): - self._enum_type = enum_type - - def CheckValue(self, proposed_value): - if not isinstance(proposed_value, numbers.Integral): - message = ('%.1024r has type %s, but expected one of: %s' % - (proposed_value, type(proposed_value), (int,))) - raise TypeError(message) - if int(proposed_value) not in self._enum_type.values_by_number: - raise ValueError('Unknown enum value: %d' % proposed_value) - return proposed_value - - def DefaultValue(self): - return self._enum_type.values[0].number - - -class UnicodeValueChecker(object): - - """Checker used for string fields. - - Always returns a unicode value, even if the input is of type str. - """ - - def CheckValue(self, proposed_value): - if not isinstance(proposed_value, (bytes, str)): - message = ('%.1024r has type %s, but expected one of: %s' % - (proposed_value, type(proposed_value), (bytes, str))) - raise TypeError(message) - - # If the value is of type 'bytes' make sure that it is valid UTF-8 data. - if isinstance(proposed_value, bytes): - try: - proposed_value = proposed_value.decode('utf-8') - except UnicodeDecodeError: - raise ValueError('%.1024r has type bytes, but isn\'t valid UTF-8 ' - 'encoding. Non-UTF-8 strings must be converted to ' - 'unicode objects before being added.' % - (proposed_value)) - else: - try: - proposed_value.encode('utf8') - except UnicodeEncodeError: - raise ValueError('%.1024r isn\'t a valid unicode string and ' - 'can\'t be encoded in UTF-8.'% - (proposed_value)) - - return proposed_value - - def DefaultValue(self): - return u"" - - -class Int32ValueChecker(IntValueChecker): - # We're sure to use ints instead of longs here since comparison may be more - # efficient. - _MIN = -2147483648 - _MAX = 2147483647 - - -class Uint32ValueChecker(IntValueChecker): - _MIN = 0 - _MAX = (1 << 32) - 1 - - -class Int64ValueChecker(IntValueChecker): - _MIN = -(1 << 63) - _MAX = (1 << 63) - 1 - - -class Uint64ValueChecker(IntValueChecker): - _MIN = 0 - _MAX = (1 << 64) - 1 - - -# The max 4 bytes float is about 3.4028234663852886e+38 -_FLOAT_MAX = float.fromhex('0x1.fffffep+127') -_FLOAT_MIN = -_FLOAT_MAX -_INF = float('inf') -_NEG_INF = float('-inf') - - -class DoubleValueChecker(object): - """Checker used for double fields. - - Performs type-check and range check. - """ - - def CheckValue(self, proposed_value): - """Check and convert proposed_value to float.""" - if (not hasattr(proposed_value, '__float__') and - not hasattr(proposed_value, '__index__')) or ( - type(proposed_value).__module__ == 'numpy' and - type(proposed_value).__name__ == 'ndarray'): - message = ('%.1024r has type %s, but expected one of: int, float' % - (proposed_value, type(proposed_value))) - raise TypeError(message) - return float(proposed_value) - - def DefaultValue(self): - return 0.0 - - -class FloatValueChecker(DoubleValueChecker): - """Checker used for float fields. - - Performs type-check and range check. - - Values exceeding a 32-bit float will be converted to inf/-inf. - """ - - def CheckValue(self, proposed_value): - """Check and convert proposed_value to float.""" - converted_value = super().CheckValue(proposed_value) - # This inf rounding matches the C++ proto SafeDoubleToFloat logic. - if converted_value > _FLOAT_MAX: - return _INF - if converted_value < _FLOAT_MIN: - return _NEG_INF - - return TruncateToFourByteFloat(converted_value) - -# Type-checkers for all scalar CPPTYPEs. -_VALUE_CHECKERS = { - _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(), - _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(), - _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(), - _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(), - _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(), - _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(), - _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(), - _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes), -} - - -# Map from field type to a function F, such that F(field_num, value) -# gives the total byte size for a value of the given type. This -# byte size includes tag information and any other additional space -# associated with serializing "value". -TYPE_TO_BYTE_SIZE_FN = { - _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize, - _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize, - _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize, - _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize, - _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize, - _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize, - _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize, - _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize, - _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize, - _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize, - _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize, - _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize, - _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize, - _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize, - _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize, - _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize, - _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize, - _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize - } - - -# Maps from field types to encoder constructors. -TYPE_TO_ENCODER = { - _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder, - _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder, - _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder, - _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder, - _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder, - _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder, - _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder, - _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder, - _FieldDescriptor.TYPE_STRING: encoder.StringEncoder, - _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder, - _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder, - _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder, - _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder, - _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder, - _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder, - _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder, - _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder, - _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder, - } - - -# Maps from field types to sizer constructors. -TYPE_TO_SIZER = { - _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer, - _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer, - _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer, - _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer, - _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer, - _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer, - _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer, - _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer, - _FieldDescriptor.TYPE_STRING: encoder.StringSizer, - _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer, - _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer, - _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer, - _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer, - _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer, - _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer, - _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer, - _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer, - _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer, - } - - -# Maps from field type to a decoder constructor. -TYPE_TO_DECODER = { - _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder, - _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder, - _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder, - _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder, - _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder, - _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder, - _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder, - _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder, - _FieldDescriptor.TYPE_STRING: decoder.StringDecoder, - _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder, - _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder, - _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder, - _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder, - _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder, - _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder, - _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder, - _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder, - _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder, - } - -# Maps from field type to expected wiretype. -FIELD_TYPE_TO_WIRE_TYPE = { - _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64, - _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32, - _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT, - _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT, - _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT, - _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64, - _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32, - _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT, - _FieldDescriptor.TYPE_STRING: - wire_format.WIRETYPE_LENGTH_DELIMITED, - _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP, - _FieldDescriptor.TYPE_MESSAGE: - wire_format.WIRETYPE_LENGTH_DELIMITED, - _FieldDescriptor.TYPE_BYTES: - wire_format.WIRETYPE_LENGTH_DELIMITED, - _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT, - _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT, - _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32, - _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64, - _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT, - _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT, - } diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/well_known_types.py b/script.module.protobuf/resources/lib/google/protobuf/internal/well_known_types.py deleted file mode 100644 index e340f9087..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/well_known_types.py +++ /dev/null @@ -1,582 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains well known classes. - -This files defines well known classes which need extra maintenance including: - - Any - - Duration - - FieldMask - - Struct - - Timestamp -""" - -__author__ = 'jieluo@google.com (Jie Luo)' - -import calendar -import collections.abc -import datetime - -from google.protobuf.internal import field_mask - -FieldMask = field_mask.FieldMask - -_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' -_NANOS_PER_SECOND = 1000000000 -_NANOS_PER_MILLISECOND = 1000000 -_NANOS_PER_MICROSECOND = 1000 -_MILLIS_PER_SECOND = 1000 -_MICROS_PER_SECOND = 1000000 -_SECONDS_PER_DAY = 24 * 3600 -_DURATION_SECONDS_MAX = 315576000000 - - -class Any(object): - """Class for Any Message type.""" - - __slots__ = () - - def Pack(self, msg, type_url_prefix='type.googleapis.com/', - deterministic=None): - """Packs the specified message into current Any message.""" - if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/': - self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) - else: - self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) - self.value = msg.SerializeToString(deterministic=deterministic) - - def Unpack(self, msg): - """Unpacks the current Any message into specified message.""" - descriptor = msg.DESCRIPTOR - if not self.Is(descriptor): - return False - msg.ParseFromString(self.value) - return True - - def TypeName(self): - """Returns the protobuf type name of the inner message.""" - # Only last part is to be used: b/25630112 - return self.type_url.split('/')[-1] - - def Is(self, descriptor): - """Checks if this Any represents the given protobuf type.""" - return '/' in self.type_url and self.TypeName() == descriptor.full_name - - -_EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0) -_EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp( - 0, tz=datetime.timezone.utc) - - -class Timestamp(object): - """Class for Timestamp message type.""" - - __slots__ = () - - def ToJsonString(self): - """Converts Timestamp to RFC 3339 date string format. - - Returns: - A string converted from timestamp. The string is always Z-normalized - and uses 3, 6 or 9 fractional digits as required to represent the - exact time. Example of the return format: '1972-01-01T10:00:20.021Z' - """ - nanos = self.nanos % _NANOS_PER_SECOND - total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND - seconds = total_sec % _SECONDS_PER_DAY - days = (total_sec - seconds) // _SECONDS_PER_DAY - dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) - - result = dt.isoformat() - if (nanos % 1e9) == 0: - # If there are 0 fractional digits, the fractional - # point '.' should be omitted when serializing. - return result + 'Z' - if (nanos % 1e6) == 0: - # Serialize 3 fractional digits. - return result + '.%03dZ' % (nanos / 1e6) - if (nanos % 1e3) == 0: - # Serialize 6 fractional digits. - return result + '.%06dZ' % (nanos / 1e3) - # Serialize 9 fractional digits. - return result + '.%09dZ' % nanos - - def FromJsonString(self, value): - """Parse a RFC 3339 date string format to Timestamp. - - Args: - value: A date string. Any fractional digits (or none) and any offset are - accepted as long as they fit into nano-seconds precision. - Example of accepted format: '1972-01-01T10:00:20.021-05:00' - - Raises: - ValueError: On parsing problems. - """ - if not isinstance(value, str): - raise ValueError('Timestamp JSON value not a string: {!r}'.format(value)) - timezone_offset = value.find('Z') - if timezone_offset == -1: - timezone_offset = value.find('+') - if timezone_offset == -1: - timezone_offset = value.rfind('-') - if timezone_offset == -1: - raise ValueError( - 'Failed to parse timestamp: missing valid timezone offset.') - time_value = value[0:timezone_offset] - # Parse datetime and nanos. - point_position = time_value.find('.') - if point_position == -1: - second_value = time_value - nano_value = '' - else: - second_value = time_value[:point_position] - nano_value = time_value[point_position + 1:] - if 't' in second_value: - raise ValueError( - 'time data \'{0}\' does not match format \'%Y-%m-%dT%H:%M:%S\', ' - 'lowercase \'t\' is not accepted'.format(second_value)) - date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT) - td = date_object - datetime.datetime(1970, 1, 1) - seconds = td.seconds + td.days * _SECONDS_PER_DAY - if len(nano_value) > 9: - raise ValueError( - 'Failed to parse Timestamp: nanos {0} more than ' - '9 fractional digits.'.format(nano_value)) - if nano_value: - nanos = round(float('0.' + nano_value) * 1e9) - else: - nanos = 0 - # Parse timezone offsets. - if value[timezone_offset] == 'Z': - if len(value) != timezone_offset + 1: - raise ValueError('Failed to parse timestamp: invalid trailing' - ' data {0}.'.format(value)) - else: - timezone = value[timezone_offset:] - pos = timezone.find(':') - if pos == -1: - raise ValueError( - 'Invalid timezone offset value: {0}.'.format(timezone)) - if timezone[0] == '+': - seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 - else: - seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 - # Set seconds and nanos - self.seconds = int(seconds) - self.nanos = int(nanos) - - def GetCurrentTime(self): - """Get the current UTC into Timestamp.""" - self.FromDatetime(datetime.datetime.utcnow()) - - def ToNanoseconds(self): - """Converts Timestamp to nanoseconds since epoch.""" - return self.seconds * _NANOS_PER_SECOND + self.nanos - - def ToMicroseconds(self): - """Converts Timestamp to microseconds since epoch.""" - return (self.seconds * _MICROS_PER_SECOND + - self.nanos // _NANOS_PER_MICROSECOND) - - def ToMilliseconds(self): - """Converts Timestamp to milliseconds since epoch.""" - return (self.seconds * _MILLIS_PER_SECOND + - self.nanos // _NANOS_PER_MILLISECOND) - - def ToSeconds(self): - """Converts Timestamp to seconds since epoch.""" - return self.seconds - - def FromNanoseconds(self, nanos): - """Converts nanoseconds since epoch to Timestamp.""" - self.seconds = nanos // _NANOS_PER_SECOND - self.nanos = nanos % _NANOS_PER_SECOND - - def FromMicroseconds(self, micros): - """Converts microseconds since epoch to Timestamp.""" - self.seconds = micros // _MICROS_PER_SECOND - self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND - - def FromMilliseconds(self, millis): - """Converts milliseconds since epoch to Timestamp.""" - self.seconds = millis // _MILLIS_PER_SECOND - self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND - - def FromSeconds(self, seconds): - """Converts seconds since epoch to Timestamp.""" - self.seconds = seconds - self.nanos = 0 - - def ToDatetime(self, tzinfo=None): - """Converts Timestamp to a datetime. - - Args: - tzinfo: A datetime.tzinfo subclass; defaults to None. - - Returns: - If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone - information, i.e. not aware that it's UTC). - - Otherwise, returns a timezone-aware datetime in the input timezone. - """ - delta = datetime.timedelta( - seconds=self.seconds, - microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)) - if tzinfo is None: - return _EPOCH_DATETIME_NAIVE + delta - else: - return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta - - def FromDatetime(self, dt): - """Converts datetime to Timestamp. - - Args: - dt: A datetime. If it's timezone-naive, it's assumed to be in UTC. - """ - # Using this guide: http://wiki.python.org/moin/WorkingWithTime - # And this conversion guide: http://docs.python.org/library/time.html - - # Turn the date parameter into a tuple (struct_time) that can then be - # manipulated into a long value of seconds. During the conversion from - # struct_time to long, the source date in UTC, and so it follows that the - # correct transformation is calendar.timegm() - self.seconds = calendar.timegm(dt.utctimetuple()) - self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND - - -class Duration(object): - """Class for Duration message type.""" - - __slots__ = () - - def ToJsonString(self): - """Converts Duration to string format. - - Returns: - A string converted from self. The string format will contains - 3, 6, or 9 fractional digits depending on the precision required to - represent the exact Duration value. For example: "1s", "1.010s", - "1.000000100s", "-3.100s" - """ - _CheckDurationValid(self.seconds, self.nanos) - if self.seconds < 0 or self.nanos < 0: - result = '-' - seconds = - self.seconds + int((0 - self.nanos) // 1e9) - nanos = (0 - self.nanos) % 1e9 - else: - result = '' - seconds = self.seconds + int(self.nanos // 1e9) - nanos = self.nanos % 1e9 - result += '%d' % seconds - if (nanos % 1e9) == 0: - # If there are 0 fractional digits, the fractional - # point '.' should be omitted when serializing. - return result + 's' - if (nanos % 1e6) == 0: - # Serialize 3 fractional digits. - return result + '.%03ds' % (nanos / 1e6) - if (nanos % 1e3) == 0: - # Serialize 6 fractional digits. - return result + '.%06ds' % (nanos / 1e3) - # Serialize 9 fractional digits. - return result + '.%09ds' % nanos - - def FromJsonString(self, value): - """Converts a string to Duration. - - Args: - value: A string to be converted. The string must end with 's'. Any - fractional digits (or none) are accepted as long as they fit into - precision. For example: "1s", "1.01s", "1.0000001s", "-3.100s - - Raises: - ValueError: On parsing problems. - """ - if not isinstance(value, str): - raise ValueError('Duration JSON value not a string: {!r}'.format(value)) - if len(value) < 1 or value[-1] != 's': - raise ValueError( - 'Duration must end with letter "s": {0}.'.format(value)) - try: - pos = value.find('.') - if pos == -1: - seconds = int(value[:-1]) - nanos = 0 - else: - seconds = int(value[:pos]) - if value[0] == '-': - nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9)) - else: - nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9)) - _CheckDurationValid(seconds, nanos) - self.seconds = seconds - self.nanos = nanos - except ValueError as e: - raise ValueError( - 'Couldn\'t parse duration: {0} : {1}.'.format(value, e)) - - def ToNanoseconds(self): - """Converts a Duration to nanoseconds.""" - return self.seconds * _NANOS_PER_SECOND + self.nanos - - def ToMicroseconds(self): - """Converts a Duration to microseconds.""" - micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND) - return self.seconds * _MICROS_PER_SECOND + micros - - def ToMilliseconds(self): - """Converts a Duration to milliseconds.""" - millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND) - return self.seconds * _MILLIS_PER_SECOND + millis - - def ToSeconds(self): - """Converts a Duration to seconds.""" - return self.seconds - - def FromNanoseconds(self, nanos): - """Converts nanoseconds to Duration.""" - self._NormalizeDuration(nanos // _NANOS_PER_SECOND, - nanos % _NANOS_PER_SECOND) - - def FromMicroseconds(self, micros): - """Converts microseconds to Duration.""" - self._NormalizeDuration( - micros // _MICROS_PER_SECOND, - (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND) - - def FromMilliseconds(self, millis): - """Converts milliseconds to Duration.""" - self._NormalizeDuration( - millis // _MILLIS_PER_SECOND, - (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND) - - def FromSeconds(self, seconds): - """Converts seconds to Duration.""" - self.seconds = seconds - self.nanos = 0 - - def ToTimedelta(self): - """Converts Duration to timedelta.""" - return datetime.timedelta( - seconds=self.seconds, microseconds=_RoundTowardZero( - self.nanos, _NANOS_PER_MICROSECOND)) - - def FromTimedelta(self, td): - """Converts timedelta to Duration.""" - self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY, - td.microseconds * _NANOS_PER_MICROSECOND) - - def _NormalizeDuration(self, seconds, nanos): - """Set Duration by seconds and nanos.""" - # Force nanos to be negative if the duration is negative. - if seconds < 0 and nanos > 0: - seconds += 1 - nanos -= _NANOS_PER_SECOND - self.seconds = seconds - self.nanos = nanos - - -def _CheckDurationValid(seconds, nanos): - if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX: - raise ValueError( - 'Duration is not valid: Seconds {0} must be in range ' - '[-315576000000, 315576000000].'.format(seconds)) - if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND: - raise ValueError( - 'Duration is not valid: Nanos {0} must be in range ' - '[-999999999, 999999999].'.format(nanos)) - if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0): - raise ValueError( - 'Duration is not valid: Sign mismatch.') - - -def _RoundTowardZero(value, divider): - """Truncates the remainder part after division.""" - # For some languages, the sign of the remainder is implementation - # dependent if any of the operands is negative. Here we enforce - # "rounded toward zero" semantics. For example, for (-5) / 2 an - # implementation may give -3 as the result with the remainder being - # 1. This function ensures we always return -2 (closer to zero). - result = value // divider - remainder = value % divider - if result < 0 and remainder > 0: - return result + 1 - else: - return result - - -def _SetStructValue(struct_value, value): - if value is None: - struct_value.null_value = 0 - elif isinstance(value, bool): - # Note: this check must come before the number check because in Python - # True and False are also considered numbers. - struct_value.bool_value = value - elif isinstance(value, str): - struct_value.string_value = value - elif isinstance(value, (int, float)): - struct_value.number_value = value - elif isinstance(value, (dict, Struct)): - struct_value.struct_value.Clear() - struct_value.struct_value.update(value) - elif isinstance(value, (list, ListValue)): - struct_value.list_value.Clear() - struct_value.list_value.extend(value) - else: - raise ValueError('Unexpected type') - - -def _GetStructValue(struct_value): - which = struct_value.WhichOneof('kind') - if which == 'struct_value': - return struct_value.struct_value - elif which == 'null_value': - return None - elif which == 'number_value': - return struct_value.number_value - elif which == 'string_value': - return struct_value.string_value - elif which == 'bool_value': - return struct_value.bool_value - elif which == 'list_value': - return struct_value.list_value - elif which is None: - raise ValueError('Value not set') - - -class Struct(object): - """Class for Struct message type.""" - - __slots__ = () - - def __getitem__(self, key): - return _GetStructValue(self.fields[key]) - - def __contains__(self, item): - return item in self.fields - - def __setitem__(self, key, value): - _SetStructValue(self.fields[key], value) - - def __delitem__(self, key): - del self.fields[key] - - def __len__(self): - return len(self.fields) - - def __iter__(self): - return iter(self.fields) - - def keys(self): # pylint: disable=invalid-name - return self.fields.keys() - - def values(self): # pylint: disable=invalid-name - return [self[key] for key in self] - - def items(self): # pylint: disable=invalid-name - return [(key, self[key]) for key in self] - - def get_or_create_list(self, key): - """Returns a list for this key, creating if it didn't exist already.""" - if not self.fields[key].HasField('list_value'): - # Clear will mark list_value modified which will indeed create a list. - self.fields[key].list_value.Clear() - return self.fields[key].list_value - - def get_or_create_struct(self, key): - """Returns a struct for this key, creating if it didn't exist already.""" - if not self.fields[key].HasField('struct_value'): - # Clear will mark struct_value modified which will indeed create a struct. - self.fields[key].struct_value.Clear() - return self.fields[key].struct_value - - def update(self, dictionary): # pylint: disable=invalid-name - for key, value in dictionary.items(): - _SetStructValue(self.fields[key], value) - -collections.abc.MutableMapping.register(Struct) - - -class ListValue(object): - """Class for ListValue message type.""" - - __slots__ = () - - def __len__(self): - return len(self.values) - - def append(self, value): - _SetStructValue(self.values.add(), value) - - def extend(self, elem_seq): - for value in elem_seq: - self.append(value) - - def __getitem__(self, index): - """Retrieves item by the specified index.""" - return _GetStructValue(self.values.__getitem__(index)) - - def __setitem__(self, index, value): - _SetStructValue(self.values.__getitem__(index), value) - - def __delitem__(self, key): - del self.values[key] - - def items(self): - for i in range(len(self)): - yield self[i] - - def add_struct(self): - """Appends and returns a struct value as the next value in the list.""" - struct_value = self.values.add().struct_value - # Clear will mark struct_value modified which will indeed create a struct. - struct_value.Clear() - return struct_value - - def add_list(self): - """Appends and returns a list value as the next value in the list.""" - list_value = self.values.add().list_value - # Clear will mark list_value modified which will indeed create a list. - list_value.Clear() - return list_value - -collections.abc.MutableSequence.register(ListValue) - - -# LINT.IfChange(wktbases) -WKTBASES = { - 'google.protobuf.Any': Any, - 'google.protobuf.Duration': Duration, - 'google.protobuf.FieldMask': FieldMask, - 'google.protobuf.ListValue': ListValue, - 'google.protobuf.Struct': Struct, - 'google.protobuf.Timestamp': Timestamp, -} -# LINT.ThenChange(//depot/google.protobuf/compiler/python/pyi_generator.cc:wktbases) diff --git a/script.module.protobuf/resources/lib/google/protobuf/internal/wire_format.py b/script.module.protobuf/resources/lib/google/protobuf/internal/wire_format.py deleted file mode 100644 index 1f54414b1..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/internal/wire_format.py +++ /dev/null @@ -1,268 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Constants and static functions to support protocol buffer wire format.""" - -__author__ = 'robinson@google.com (Will Robinson)' - -import struct -from google.protobuf import descriptor -from google.protobuf import message - - -TAG_TYPE_BITS = 3 # Number of bits used to hold type info in a proto tag. -TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1 # 0x7 - -# These numbers identify the wire type of a protocol buffer value. -# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded -# tag-and-type to store one of these WIRETYPE_* constants. -# These values must match WireType enum in //google/protobuf/wire_format.h. -WIRETYPE_VARINT = 0 -WIRETYPE_FIXED64 = 1 -WIRETYPE_LENGTH_DELIMITED = 2 -WIRETYPE_START_GROUP = 3 -WIRETYPE_END_GROUP = 4 -WIRETYPE_FIXED32 = 5 -_WIRETYPE_MAX = 5 - - -# Bounds for various integer types. -INT32_MAX = int((1 << 31) - 1) -INT32_MIN = int(-(1 << 31)) -UINT32_MAX = (1 << 32) - 1 - -INT64_MAX = (1 << 63) - 1 -INT64_MIN = -(1 << 63) -UINT64_MAX = (1 << 64) - 1 - -# "struct" format strings that will encode/decode the specified formats. -FORMAT_UINT32_LITTLE_ENDIAN = '> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK) - - -def ZigZagEncode(value): - """ZigZag Transform: Encodes signed integers so that they can be - effectively used with varint encoding. See wire_format.h for - more details. - """ - if value >= 0: - return value << 1 - return (value << 1) ^ (~0) - - -def ZigZagDecode(value): - """Inverse of ZigZagEncode().""" - if not value & 0x1: - return value >> 1 - return (value >> 1) ^ (~0) - - - -# The *ByteSize() functions below return the number of bytes required to -# serialize "field number + type" information and then serialize the value. - - -def Int32ByteSize(field_number, int32): - return Int64ByteSize(field_number, int32) - - -def Int32ByteSizeNoTag(int32): - return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32) - - -def Int64ByteSize(field_number, int64): - # Have to convert to uint before calling UInt64ByteSize(). - return UInt64ByteSize(field_number, 0xffffffffffffffff & int64) - - -def UInt32ByteSize(field_number, uint32): - return UInt64ByteSize(field_number, uint32) - - -def UInt64ByteSize(field_number, uint64): - return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64) - - -def SInt32ByteSize(field_number, int32): - return UInt32ByteSize(field_number, ZigZagEncode(int32)) - - -def SInt64ByteSize(field_number, int64): - return UInt64ByteSize(field_number, ZigZagEncode(int64)) - - -def Fixed32ByteSize(field_number, fixed32): - return TagByteSize(field_number) + 4 - - -def Fixed64ByteSize(field_number, fixed64): - return TagByteSize(field_number) + 8 - - -def SFixed32ByteSize(field_number, sfixed32): - return TagByteSize(field_number) + 4 - - -def SFixed64ByteSize(field_number, sfixed64): - return TagByteSize(field_number) + 8 - - -def FloatByteSize(field_number, flt): - return TagByteSize(field_number) + 4 - - -def DoubleByteSize(field_number, double): - return TagByteSize(field_number) + 8 - - -def BoolByteSize(field_number, b): - return TagByteSize(field_number) + 1 - - -def EnumByteSize(field_number, enum): - return UInt32ByteSize(field_number, enum) - - -def StringByteSize(field_number, string): - return BytesByteSize(field_number, string.encode('utf-8')) - - -def BytesByteSize(field_number, b): - return (TagByteSize(field_number) - + _VarUInt64ByteSizeNoTag(len(b)) - + len(b)) - - -def GroupByteSize(field_number, message): - return (2 * TagByteSize(field_number) # START and END group. - + message.ByteSize()) - - -def MessageByteSize(field_number, message): - return (TagByteSize(field_number) - + _VarUInt64ByteSizeNoTag(message.ByteSize()) - + message.ByteSize()) - - -def MessageSetItemByteSize(field_number, msg): - # First compute the sizes of the tags. - # There are 2 tags for the beginning and ending of the repeated group, that - # is field number 1, one with field number 2 (type_id) and one with field - # number 3 (message). - total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3)) - - # Add the number of bytes for type_id. - total_size += _VarUInt64ByteSizeNoTag(field_number) - - message_size = msg.ByteSize() - - # The number of bytes for encoding the length of the message. - total_size += _VarUInt64ByteSizeNoTag(message_size) - - # The size of the message. - total_size += message_size - return total_size - - -def TagByteSize(field_number): - """Returns the bytes required to serialize a tag with this field number.""" - # Just pass in type 0, since the type won't affect the tag+type size. - return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0)) - - -# Private helper function for the *ByteSize() functions above. - -def _VarUInt64ByteSizeNoTag(uint64): - """Returns the number of bytes required to serialize a single varint - using boundary value comparisons. (unrolled loop optimization -WPierce) - uint64 must be unsigned. - """ - if uint64 <= 0x7f: return 1 - if uint64 <= 0x3fff: return 2 - if uint64 <= 0x1fffff: return 3 - if uint64 <= 0xfffffff: return 4 - if uint64 <= 0x7ffffffff: return 5 - if uint64 <= 0x3ffffffffff: return 6 - if uint64 <= 0x1ffffffffffff: return 7 - if uint64 <= 0xffffffffffffff: return 8 - if uint64 <= 0x7fffffffffffffff: return 9 - if uint64 > UINT64_MAX: - raise message.EncodeError('Value out of range: %d' % uint64) - return 10 - - -NON_PACKABLE_TYPES = ( - descriptor.FieldDescriptor.TYPE_STRING, - descriptor.FieldDescriptor.TYPE_GROUP, - descriptor.FieldDescriptor.TYPE_MESSAGE, - descriptor.FieldDescriptor.TYPE_BYTES -) - - -def IsTypePackable(field_type): - """Return true iff packable = true is valid for fields of this type. - - Args: - field_type: a FieldDescriptor::Type value. - - Returns: - True iff fields of this type are packable. - """ - return field_type not in NON_PACKABLE_TYPES diff --git a/script.module.protobuf/resources/lib/google/protobuf/json_format.py b/script.module.protobuf/resources/lib/google/protobuf/json_format.py deleted file mode 100644 index a04e8aef1..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/json_format.py +++ /dev/null @@ -1,929 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains routines for printing protocol messages in JSON format. - -Simple usage example: - - # Create a proto object and serialize it to a json format string. - message = my_proto_pb2.MyMessage(foo='bar') - json_string = json_format.MessageToJson(message) - - # Parse a json format string to proto object. - message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) -""" - -__author__ = 'jieluo@google.com (Jie Luo)' - - -import base64 -from collections import OrderedDict -import json -import math -from operator import methodcaller -import re -import sys - -from google.protobuf.internal import type_checkers -from google.protobuf import descriptor -from google.protobuf import message_factory -from google.protobuf import symbol_database - - -_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' -_INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32, - descriptor.FieldDescriptor.CPPTYPE_UINT32, - descriptor.FieldDescriptor.CPPTYPE_INT64, - descriptor.FieldDescriptor.CPPTYPE_UINT64]) -_INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64, - descriptor.FieldDescriptor.CPPTYPE_UINT64]) -_FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT, - descriptor.FieldDescriptor.CPPTYPE_DOUBLE]) -_INFINITY = 'Infinity' -_NEG_INFINITY = '-Infinity' -_NAN = 'NaN' - -_UNPAIRED_SURROGATE_PATTERN = re.compile( - u'[\ud800-\udbff](?![\udc00-\udfff])|(? self.max_recursion_depth: - raise ParseError('Message too deep. Max recursion depth is {0}'.format( - self.max_recursion_depth)) - message_descriptor = message.DESCRIPTOR - full_name = message_descriptor.full_name - if not path: - path = message_descriptor.name - if _IsWrapperMessage(message_descriptor): - self._ConvertWrapperMessage(value, message, path) - elif full_name in _WKTJSONMETHODS: - methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self) - else: - self._ConvertFieldValuePair(value, message, path) - self.recursion_depth -= 1 - - def _ConvertFieldValuePair(self, js, message, path): - """Convert field value pairs into regular message. - - Args: - js: A JSON object to convert the field value pairs. - message: A regular protocol message to record the data. - path: parent path to log parse error info. - - Raises: - ParseError: In case of problems converting. - """ - names = [] - message_descriptor = message.DESCRIPTOR - fields_by_json_name = dict((f.json_name, f) - for f in message_descriptor.fields) - for name in js: - try: - field = fields_by_json_name.get(name, None) - if not field: - field = message_descriptor.fields_by_name.get(name, None) - if not field and _VALID_EXTENSION_NAME.match(name): - if not message_descriptor.is_extendable: - raise ParseError( - 'Message type {0} does not have extensions at {1}'.format( - message_descriptor.full_name, path)) - identifier = name[1:-1] # strip [] brackets - # pylint: disable=protected-access - field = message.Extensions._FindExtensionByName(identifier) - # pylint: enable=protected-access - if not field: - # Try looking for extension by the message type name, dropping the - # field name following the final . separator in full_name. - identifier = '.'.join(identifier.split('.')[:-1]) - # pylint: disable=protected-access - field = message.Extensions._FindExtensionByName(identifier) - # pylint: enable=protected-access - if not field: - if self.ignore_unknown_fields: - continue - raise ParseError( - ('Message type "{0}" has no field named "{1}" at "{2}".\n' - ' Available Fields(except extensions): "{3}"').format( - message_descriptor.full_name, name, path, - [f.json_name for f in message_descriptor.fields])) - if name in names: - raise ParseError('Message type "{0}" should not have multiple ' - '"{1}" fields at "{2}".'.format( - message.DESCRIPTOR.full_name, name, path)) - names.append(name) - value = js[name] - # Check no other oneof field is parsed. - if field.containing_oneof is not None and value is not None: - oneof_name = field.containing_oneof.name - if oneof_name in names: - raise ParseError('Message type "{0}" should not have multiple ' - '"{1}" oneof fields at "{2}".'.format( - message.DESCRIPTOR.full_name, oneof_name, - path)) - names.append(oneof_name) - - if value is None: - if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE - and field.message_type.full_name == 'google.protobuf.Value'): - sub_message = getattr(message, field.name) - sub_message.null_value = 0 - elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM - and field.enum_type.full_name == 'google.protobuf.NullValue'): - setattr(message, field.name, 0) - else: - message.ClearField(field.name) - continue - - # Parse field value. - if _IsMapEntry(field): - message.ClearField(field.name) - self._ConvertMapFieldValue(value, message, field, - '{0}.{1}'.format(path, name)) - elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: - message.ClearField(field.name) - if not isinstance(value, list): - raise ParseError('repeated field {0} must be in [] which is ' - '{1} at {2}'.format(name, value, path)) - if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: - # Repeated message field. - for index, item in enumerate(value): - sub_message = getattr(message, field.name).add() - # None is a null_value in Value. - if (item is None and - sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'): - raise ParseError('null is not allowed to be used as an element' - ' in a repeated field at {0}.{1}[{2}]'.format( - path, name, index)) - self.ConvertMessage(item, sub_message, - '{0}.{1}[{2}]'.format(path, name, index)) - else: - # Repeated scalar field. - for index, item in enumerate(value): - if item is None: - raise ParseError('null is not allowed to be used as an element' - ' in a repeated field at {0}.{1}[{2}]'.format( - path, name, index)) - getattr(message, field.name).append( - _ConvertScalarFieldValue( - item, field, '{0}.{1}[{2}]'.format(path, name, index))) - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: - if field.is_extension: - sub_message = message.Extensions[field] - else: - sub_message = getattr(message, field.name) - sub_message.SetInParent() - self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name)) - else: - if field.is_extension: - message.Extensions[field] = _ConvertScalarFieldValue( - value, field, '{0}.{1}'.format(path, name)) - else: - setattr( - message, field.name, - _ConvertScalarFieldValue(value, field, - '{0}.{1}'.format(path, name))) - except ParseError as e: - if field and field.containing_oneof is None: - raise ParseError( - 'Failed to parse {0} field: {1}.'.format(name, e) - ) from e - else: - raise ParseError(str(e)) from e - except ValueError as e: - raise ParseError( - 'Failed to parse {0} field: {1}.'.format(name, e) - ) from e - except TypeError as e: - raise ParseError( - 'Failed to parse {0} field: {1}.'.format(name, e) - ) from e - - def _ConvertAnyMessage(self, value, message, path): - """Convert a JSON representation into Any message.""" - if isinstance(value, dict) and not value: - return - try: - type_url = value['@type'] - except KeyError as e: - raise ParseError( - '@type is missing when parsing any message at {0}'.format(path) - ) from e - - try: - sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) - except TypeError as e: - raise ParseError('{0} at {1}'.format(e, path)) from e - message_descriptor = sub_message.DESCRIPTOR - full_name = message_descriptor.full_name - if _IsWrapperMessage(message_descriptor): - self._ConvertWrapperMessage(value['value'], sub_message, - '{0}.value'.format(path)) - elif full_name in _WKTJSONMETHODS: - methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message, - '{0}.value'.format(path))( - self) - else: - del value['@type'] - self._ConvertFieldValuePair(value, sub_message, path) - value['@type'] = type_url - # Sets Any message - message.value = sub_message.SerializeToString() - message.type_url = type_url - - def _ConvertGenericMessage(self, value, message, path): - """Convert a JSON representation into message with FromJsonString.""" - # Duration, Timestamp, FieldMask have a FromJsonString method to do the - # conversion. Users can also call the method directly. - try: - message.FromJsonString(value) - except ValueError as e: - raise ParseError('{0} at {1}'.format(e, path)) from e - - def _ConvertValueMessage(self, value, message, path): - """Convert a JSON representation into Value message.""" - if isinstance(value, dict): - self._ConvertStructMessage(value, message.struct_value, path) - elif isinstance(value, list): - self._ConvertListValueMessage(value, message.list_value, path) - elif value is None: - message.null_value = 0 - elif isinstance(value, bool): - message.bool_value = value - elif isinstance(value, str): - message.string_value = value - elif isinstance(value, _INT_OR_FLOAT): - message.number_value = value - else: - raise ParseError('Value {0} has unexpected type {1} at {2}'.format( - value, type(value), path)) - - def _ConvertListValueMessage(self, value, message, path): - """Convert a JSON representation into ListValue message.""" - if not isinstance(value, list): - raise ParseError('ListValue must be in [] which is {0} at {1}'.format( - value, path)) - message.ClearField('values') - for index, item in enumerate(value): - self._ConvertValueMessage(item, message.values.add(), - '{0}[{1}]'.format(path, index)) - - def _ConvertStructMessage(self, value, message, path): - """Convert a JSON representation into Struct message.""" - if not isinstance(value, dict): - raise ParseError('Struct must be in a dict which is {0} at {1}'.format( - value, path)) - # Clear will mark the struct as modified so it will be created even if - # there are no values. - message.Clear() - for key in value: - self._ConvertValueMessage(value[key], message.fields[key], - '{0}.{1}'.format(path, key)) - return - - def _ConvertWrapperMessage(self, value, message, path): - """Convert a JSON representation into Wrapper message.""" - field = message.DESCRIPTOR.fields_by_name['value'] - setattr( - message, 'value', - _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path))) - - def _ConvertMapFieldValue(self, value, message, field, path): - """Convert map field value for a message map field. - - Args: - value: A JSON object to convert the map field value. - message: A protocol message to record the converted data. - field: The descriptor of the map field to be converted. - path: parent path to log parse error info. - - Raises: - ParseError: In case of convert problems. - """ - if not isinstance(value, dict): - raise ParseError( - 'Map field {0} must be in a dict which is {1} at {2}'.format( - field.name, value, path)) - key_field = field.message_type.fields_by_name['key'] - value_field = field.message_type.fields_by_name['value'] - for key in value: - key_value = _ConvertScalarFieldValue(key, key_field, - '{0}.key'.format(path), True) - if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: - self.ConvertMessage(value[key], - getattr(message, field.name)[key_value], - '{0}[{1}]'.format(path, key_value)) - else: - getattr(message, field.name)[key_value] = _ConvertScalarFieldValue( - value[key], value_field, path='{0}[{1}]'.format(path, key_value)) - - -def _ConvertScalarFieldValue(value, field, path, require_str=False): - """Convert a single scalar field value. - - Args: - value: A scalar value to convert the scalar field value. - field: The descriptor of the field to convert. - path: parent path to log parse error info. - require_str: If True, the field value must be a str. - - Returns: - The converted scalar field value - - Raises: - ParseError: In case of convert problems. - """ - try: - if field.cpp_type in _INT_TYPES: - return _ConvertInteger(value) - elif field.cpp_type in _FLOAT_TYPES: - return _ConvertFloat(value, field) - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: - return _ConvertBool(value, require_str) - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: - if field.type == descriptor.FieldDescriptor.TYPE_BYTES: - if isinstance(value, str): - encoded = value.encode('utf-8') - else: - encoded = value - # Add extra padding '=' - padded_value = encoded + b'=' * (4 - len(encoded) % 4) - return base64.urlsafe_b64decode(padded_value) - else: - # Checking for unpaired surrogates appears to be unreliable, - # depending on the specific Python version, so we check manually. - if _UNPAIRED_SURROGATE_PATTERN.search(value): - raise ParseError('Unpaired surrogate') - return value - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: - # Convert an enum value. - enum_value = field.enum_type.values_by_name.get(value, None) - if enum_value is None: - try: - number = int(value) - enum_value = field.enum_type.values_by_number.get(number, None) - except ValueError as e: - raise ParseError('Invalid enum value {0} for enum type {1}'.format( - value, field.enum_type.full_name)) from e - if enum_value is None: - if field.enum_type.is_closed: - raise ParseError('Invalid enum value {0} for enum type {1}'.format( - value, field.enum_type.full_name)) - else: - return number - return enum_value.number - except ParseError as e: - raise ParseError('{0} at {1}'.format(e, path)) from e - - -def _ConvertInteger(value): - """Convert an integer. - - Args: - value: A scalar value to convert. - - Returns: - The integer value. - - Raises: - ParseError: If an integer couldn't be consumed. - """ - if isinstance(value, float) and not value.is_integer(): - raise ParseError('Couldn\'t parse integer: {0}'.format(value)) - - if isinstance(value, str) and value.find(' ') != -1: - raise ParseError('Couldn\'t parse integer: "{0}"'.format(value)) - - if isinstance(value, bool): - raise ParseError('Bool value {0} is not acceptable for ' - 'integer field'.format(value)) - - return int(value) - - -def _ConvertFloat(value, field): - """Convert an floating point number.""" - if isinstance(value, float): - if math.isnan(value): - raise ParseError('Couldn\'t parse NaN, use quoted "NaN" instead') - if math.isinf(value): - if value > 0: - raise ParseError('Couldn\'t parse Infinity or value too large, ' - 'use quoted "Infinity" instead') - else: - raise ParseError('Couldn\'t parse -Infinity or value too small, ' - 'use quoted "-Infinity" instead') - if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: - # pylint: disable=protected-access - if value > type_checkers._FLOAT_MAX: - raise ParseError('Float value too large') - # pylint: disable=protected-access - if value < type_checkers._FLOAT_MIN: - raise ParseError('Float value too small') - if value == 'nan': - raise ParseError('Couldn\'t parse float "nan", use "NaN" instead') - try: - # Assume Python compatible syntax. - return float(value) - except ValueError as e: - # Check alternative spellings. - if value == _NEG_INFINITY: - return float('-inf') - elif value == _INFINITY: - return float('inf') - elif value == _NAN: - return float('nan') - else: - raise ParseError('Couldn\'t parse float: {0}'.format(value)) from e - - -def _ConvertBool(value, require_str): - """Convert a boolean value. - - Args: - value: A scalar value to convert. - require_str: If True, value must be a str. - - Returns: - The bool parsed. - - Raises: - ParseError: If a boolean value couldn't be consumed. - """ - if require_str: - if value == 'true': - return True - elif value == 'false': - return False - else: - raise ParseError('Expected "true" or "false", not {0}'.format(value)) - - if not isinstance(value, bool): - raise ParseError('Expected true or false without quotes') - return value - -_WKTJSONMETHODS = { - 'google.protobuf.Any': ['_AnyMessageToJsonObject', - '_ConvertAnyMessage'], - 'google.protobuf.Duration': ['_GenericMessageToJsonObject', - '_ConvertGenericMessage'], - 'google.protobuf.FieldMask': ['_GenericMessageToJsonObject', - '_ConvertGenericMessage'], - 'google.protobuf.ListValue': ['_ListValueMessageToJsonObject', - '_ConvertListValueMessage'], - 'google.protobuf.Struct': ['_StructMessageToJsonObject', - '_ConvertStructMessage'], - 'google.protobuf.Timestamp': ['_GenericMessageToJsonObject', - '_ConvertGenericMessage'], - 'google.protobuf.Value': ['_ValueMessageToJsonObject', - '_ConvertValueMessage'] -} diff --git a/script.module.protobuf/resources/lib/google/protobuf/message.py b/script.module.protobuf/resources/lib/google/protobuf/message.py deleted file mode 100644 index 37b9c4054..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/message.py +++ /dev/null @@ -1,425 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# TODO(robinson): We should just make these methods all "pure-virtual" and move -# all implementation out, into reflection.py for now. - - -"""Contains an abstract base class for protocol messages.""" - -__author__ = 'robinson@google.com (Will Robinson)' - -class Error(Exception): - """Base error type for this module.""" - pass - - -class DecodeError(Error): - """Exception raised when deserializing messages.""" - pass - - -class EncodeError(Error): - """Exception raised when serializing messages.""" - pass - - -class Message(object): - - """Abstract base class for protocol messages. - - Protocol message classes are almost always generated by the protocol - compiler. These generated types subclass Message and implement the methods - shown below. - """ - - # TODO(robinson): Link to an HTML document here. - - # TODO(robinson): Document that instances of this class will also - # have an Extensions attribute with __getitem__ and __setitem__. - # Again, not sure how to best convey this. - - # TODO(robinson): Document that the class must also have a static - # RegisterExtension(extension_field) method. - # Not sure how to best express at this point. - - # TODO(robinson): Document these fields and methods. - - __slots__ = [] - - #: The :class:`google.protobuf.Descriptor` - # for this message type. - DESCRIPTOR = None - - def __deepcopy__(self, memo=None): - clone = type(self)() - clone.MergeFrom(self) - return clone - - def __eq__(self, other_msg): - """Recursively compares two messages by value and structure.""" - raise NotImplementedError - - def __ne__(self, other_msg): - # Can't just say self != other_msg, since that would infinitely recurse. :) - return not self == other_msg - - def __hash__(self): - raise TypeError('unhashable object') - - def __str__(self): - """Outputs a human-readable representation of the message.""" - raise NotImplementedError - - def __unicode__(self): - """Outputs a human-readable representation of the message.""" - raise NotImplementedError - - def MergeFrom(self, other_msg): - """Merges the contents of the specified message into current message. - - This method merges the contents of the specified message into the current - message. Singular fields that are set in the specified message overwrite - the corresponding fields in the current message. Repeated fields are - appended. Singular sub-messages and groups are recursively merged. - - Args: - other_msg (Message): A message to merge into the current message. - """ - raise NotImplementedError - - def CopyFrom(self, other_msg): - """Copies the content of the specified message into the current message. - - The method clears the current message and then merges the specified - message using MergeFrom. - - Args: - other_msg (Message): A message to copy into the current one. - """ - if self is other_msg: - return - self.Clear() - self.MergeFrom(other_msg) - - def Clear(self): - """Clears all data that was set in the message.""" - raise NotImplementedError - - def SetInParent(self): - """Mark this as present in the parent. - - This normally happens automatically when you assign a field of a - sub-message, but sometimes you want to make the sub-message - present while keeping it empty. If you find yourself using this, - you may want to reconsider your design. - """ - raise NotImplementedError - - def IsInitialized(self): - """Checks if the message is initialized. - - Returns: - bool: The method returns True if the message is initialized (i.e. all of - its required fields are set). - """ - raise NotImplementedError - - # TODO(robinson): MergeFromString() should probably return None and be - # implemented in terms of a helper that returns the # of bytes read. Our - # deserialization routines would use the helper when recursively - # deserializing, but the end user would almost always just want the no-return - # MergeFromString(). - - def MergeFromString(self, serialized): - """Merges serialized protocol buffer data into this message. - - When we find a field in `serialized` that is already present - in this message: - - - If it's a "repeated" field, we append to the end of our list. - - Else, if it's a scalar, we overwrite our field. - - Else, (it's a nonrepeated composite), we recursively merge - into the existing composite. - - Args: - serialized (bytes): Any object that allows us to call - ``memoryview(serialized)`` to access a string of bytes using the - buffer interface. - - Returns: - int: The number of bytes read from `serialized`. - For non-group messages, this will always be `len(serialized)`, - but for messages which are actually groups, this will - generally be less than `len(serialized)`, since we must - stop when we reach an ``END_GROUP`` tag. Note that if - we *do* stop because of an ``END_GROUP`` tag, the number - of bytes returned does not include the bytes - for the ``END_GROUP`` tag information. - - Raises: - DecodeError: if the input cannot be parsed. - """ - # TODO(robinson): Document handling of unknown fields. - # TODO(robinson): When we switch to a helper, this will return None. - raise NotImplementedError - - def ParseFromString(self, serialized): - """Parse serialized protocol buffer data in binary form into this message. - - Like :func:`MergeFromString()`, except we clear the object first. - - Raises: - message.DecodeError if the input cannot be parsed. - """ - self.Clear() - return self.MergeFromString(serialized) - - def SerializeToString(self, **kwargs): - """Serializes the protocol message to a binary string. - - Keyword Args: - deterministic (bool): If true, requests deterministic serialization - of the protobuf, with predictable ordering of map keys. - - Returns: - A binary string representation of the message if all of the required - fields in the message are set (i.e. the message is initialized). - - Raises: - EncodeError: if the message isn't initialized (see :func:`IsInitialized`). - """ - raise NotImplementedError - - def SerializePartialToString(self, **kwargs): - """Serializes the protocol message to a binary string. - - This method is similar to SerializeToString but doesn't check if the - message is initialized. - - Keyword Args: - deterministic (bool): If true, requests deterministic serialization - of the protobuf, with predictable ordering of map keys. - - Returns: - bytes: A serialized representation of the partial message. - """ - raise NotImplementedError - - # TODO(robinson): Decide whether we like these better - # than auto-generated has_foo() and clear_foo() methods - # on the instances themselves. This way is less consistent - # with C++, but it makes reflection-type access easier and - # reduces the number of magically autogenerated things. - # - # TODO(robinson): Be sure to document (and test) exactly - # which field names are accepted here. Are we case-sensitive? - # What do we do with fields that share names with Python keywords - # like 'lambda' and 'yield'? - # - # nnorwitz says: - # """ - # Typically (in python), an underscore is appended to names that are - # keywords. So they would become lambda_ or yield_. - # """ - def ListFields(self): - """Returns a list of (FieldDescriptor, value) tuples for present fields. - - A message field is non-empty if HasField() would return true. A singular - primitive field is non-empty if HasField() would return true in proto2 or it - is non zero in proto3. A repeated field is non-empty if it contains at least - one element. The fields are ordered by field number. - - Returns: - list[tuple(FieldDescriptor, value)]: field descriptors and values - for all fields in the message which are not empty. The values vary by - field type. - """ - raise NotImplementedError - - def HasField(self, field_name): - """Checks if a certain field is set for the message. - - For a oneof group, checks if any field inside is set. Note that if the - field_name is not defined in the message descriptor, :exc:`ValueError` will - be raised. - - Args: - field_name (str): The name of the field to check for presence. - - Returns: - bool: Whether a value has been set for the named field. - - Raises: - ValueError: if the `field_name` is not a member of this message. - """ - raise NotImplementedError - - def ClearField(self, field_name): - """Clears the contents of a given field. - - Inside a oneof group, clears the field set. If the name neither refers to a - defined field or oneof group, :exc:`ValueError` is raised. - - Args: - field_name (str): The name of the field to check for presence. - - Raises: - ValueError: if the `field_name` is not a member of this message. - """ - raise NotImplementedError - - def WhichOneof(self, oneof_group): - """Returns the name of the field that is set inside a oneof group. - - If no field is set, returns None. - - Args: - oneof_group (str): the name of the oneof group to check. - - Returns: - str or None: The name of the group that is set, or None. - - Raises: - ValueError: no group with the given name exists - """ - raise NotImplementedError - - def HasExtension(self, field_descriptor): - """Checks if a certain extension is present for this message. - - Extensions are retrieved using the :attr:`Extensions` mapping (if present). - - Args: - field_descriptor: The field descriptor for the extension to check. - - Returns: - bool: Whether the extension is present for this message. - - Raises: - KeyError: if the extension is repeated. Similar to repeated fields, - there is no separate notion of presence: a "not present" repeated - extension is an empty list. - """ - raise NotImplementedError - - def ClearExtension(self, field_descriptor): - """Clears the contents of a given extension. - - Args: - field_descriptor: The field descriptor for the extension to clear. - """ - raise NotImplementedError - - def UnknownFields(self): - """Returns the UnknownFieldSet. - - Returns: - UnknownFieldSet: The unknown fields stored in this message. - """ - raise NotImplementedError - - def DiscardUnknownFields(self): - """Clears all fields in the :class:`UnknownFieldSet`. - - This operation is recursive for nested message. - """ - raise NotImplementedError - - def ByteSize(self): - """Returns the serialized size of this message. - - Recursively calls ByteSize() on all contained messages. - - Returns: - int: The number of bytes required to serialize this message. - """ - raise NotImplementedError - - @classmethod - def FromString(cls, s): - raise NotImplementedError - - @staticmethod - def RegisterExtension(field_descriptor): - raise NotImplementedError - - def _SetListener(self, message_listener): - """Internal method used by the protocol message implementation. - Clients should not call this directly. - - Sets a listener that this message will call on certain state transitions. - - The purpose of this method is to register back-edges from children to - parents at runtime, for the purpose of setting "has" bits and - byte-size-dirty bits in the parent and ancestor objects whenever a child or - descendant object is modified. - - If the client wants to disconnect this Message from the object tree, she - explicitly sets callback to None. - - If message_listener is None, unregisters any existing listener. Otherwise, - message_listener must implement the MessageListener interface in - internal/message_listener.py, and we discard any listener registered - via a previous _SetListener() call. - """ - raise NotImplementedError - - def __getstate__(self): - """Support the pickle protocol.""" - return dict(serialized=self.SerializePartialToString()) - - def __setstate__(self, state): - """Support the pickle protocol.""" - self.__init__() - serialized = state['serialized'] - # On Python 3, using encoding='latin1' is required for unpickling - # protos pickled by Python 2. - if not isinstance(serialized, bytes): - serialized = serialized.encode('latin1') - self.ParseFromString(serialized) - - def __reduce__(self): - message_descriptor = self.DESCRIPTOR - if message_descriptor.containing_type is None: - return type(self), (), self.__getstate__() - # the message type must be nested. - # Python does not pickle nested classes; use the symbol_database on the - # receiving end. - container = message_descriptor - return (_InternalConstructMessage, (container.full_name,), - self.__getstate__()) - - -def _InternalConstructMessage(full_name): - """Constructs a nested message.""" - from google.protobuf import symbol_database # pylint:disable=g-import-not-at-top - - return symbol_database.Default().GetSymbol(full_name)() diff --git a/script.module.protobuf/resources/lib/google/protobuf/message_factory.py b/script.module.protobuf/resources/lib/google/protobuf/message_factory.py deleted file mode 100644 index 74dd4a676..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/message_factory.py +++ /dev/null @@ -1,234 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Provides a factory class for generating dynamic messages. - -The easiest way to use this class is if you have access to the FileDescriptor -protos containing the messages you want to create you can just do the following: - -message_classes = message_factory.GetMessages(iterable_of_file_descriptors) -my_proto_instance = message_classes['some.proto.package.MessageName']() -""" - -__author__ = 'matthewtoia@google.com (Matt Toia)' - -import warnings - -from google.protobuf.internal import api_implementation -from google.protobuf import descriptor_pool -from google.protobuf import message - -if api_implementation.Type() == 'python': - from google.protobuf.internal import python_message as message_impl -else: - from google.protobuf.pyext import cpp_message as message_impl # pylint: disable=g-import-not-at-top - - -# The type of all Message classes. -_GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType - - -def GetMessageClass(descriptor): - """Obtains a proto2 message class based on the passed in descriptor. - - Passing a descriptor with a fully qualified name matching a previous - invocation will cause the same class to be returned. - - Args: - descriptor: The descriptor to build from. - - Returns: - A class describing the passed in descriptor. - """ - concrete_class = getattr(descriptor, '_concrete_class', None) - if concrete_class: - return concrete_class - return _InternalCreateMessageClass(descriptor) - - -def GetMessageClassesForFiles(files, pool): - """Gets all the messages from specified files. - - This will find and resolve dependencies, failing if the descriptor - pool cannot satisfy them. - - Args: - files: The file names to extract messages from. - pool: The descriptor pool to find the files including the dependent - files. - - Returns: - A dictionary mapping proto names to the message classes. - """ - result = {} - for file_name in files: - file_desc = pool.FindFileByName(file_name) - for desc in file_desc.message_types_by_name.values(): - result[desc.full_name] = GetMessageClass(desc) - - # While the extension FieldDescriptors are created by the descriptor pool, - # the python classes created in the factory need them to be registered - # explicitly, which is done below. - # - # The call to RegisterExtension will specifically check if the - # extension was already registered on the object and either - # ignore the registration if the original was the same, or raise - # an error if they were different. - - for extension in file_desc.extensions_by_name.values(): - extended_class = GetMessageClass(extension.containing_type) - extended_class.RegisterExtension(extension) - # Recursively load protos for extension field, in order to be able to - # fully represent the extension. This matches the behavior for regular - # fields too. - if extension.message_type: - GetMessageClass(extension.message_type) - return result - - -def _InternalCreateMessageClass(descriptor): - """Builds a proto2 message class based on the passed in descriptor. - - Args: - descriptor: The descriptor to build from. - - Returns: - A class describing the passed in descriptor. - """ - descriptor_name = descriptor.name - result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE( - descriptor_name, - (message.Message,), - { - 'DESCRIPTOR': descriptor, - # If module not set, it wrongly points to message_factory module. - '__module__': None, - }) - for field in descriptor.fields: - if field.message_type: - GetMessageClass(field.message_type) - for extension in result_class.DESCRIPTOR.extensions: - extended_class = GetMessageClass(extension.containing_type) - extended_class.RegisterExtension(extension) - if extension.message_type: - GetMessageClass(extension.message_type) - return result_class - - -# Deprecated. Please use GetMessageClass() or GetMessageClassesForFiles() -# method above instead. -class MessageFactory(object): - """Factory for creating Proto2 messages from descriptors in a pool.""" - - def __init__(self, pool=None): - """Initializes a new factory.""" - self.pool = pool or descriptor_pool.DescriptorPool() - - def GetPrototype(self, descriptor): - """Obtains a proto2 message class based on the passed in descriptor. - - Passing a descriptor with a fully qualified name matching a previous - invocation will cause the same class to be returned. - - Args: - descriptor: The descriptor to build from. - - Returns: - A class describing the passed in descriptor. - """ - warnings.warn('MessageFactory class is deprecated. Please use ' - 'GetMessageClass() instead of MessageFactory.GetPrototype. ' - 'MessageFactory class will be removed after 2024.') - return GetMessageClass(descriptor) - - def CreatePrototype(self, descriptor): - """Builds a proto2 message class based on the passed in descriptor. - - Don't call this function directly, it always creates a new class. Call - GetMessageClass() instead. - - Args: - descriptor: The descriptor to build from. - - Returns: - A class describing the passed in descriptor. - """ - warnings.warn('Directly call CreatePrototype is wrong. Please use ' - 'GetMessageClass() method instead. Directly use ' - 'CreatePrototype will raise error after July 2023.') - return _InternalCreateMessageClass(descriptor) - - def GetMessages(self, files): - """Gets all the messages from a specified file. - - This will find and resolve dependencies, failing if the descriptor - pool cannot satisfy them. - - Args: - files: The file names to extract messages from. - - Returns: - A dictionary mapping proto names to the message classes. This will include - any dependent messages as well as any messages defined in the same file as - a specified message. - """ - warnings.warn('MessageFactory class is deprecated. Please use ' - 'GetMessageClassesForFiles() instead of ' - 'MessageFactory.GetMessages(). MessageFactory class ' - 'will be removed after 2024.') - return GetMessageClassesForFiles(files, self.pool) - - -def GetMessages(file_protos, pool=None): - """Builds a dictionary of all the messages available in a set of files. - - Args: - file_protos: Iterable of FileDescriptorProto to build messages out of. - pool: The descriptor pool to add the file protos. - - Returns: - A dictionary mapping proto names to the message classes. This will include - any dependent messages as well as any messages defined in the same file as - a specified message. - """ - # The cpp implementation of the protocol buffer library requires to add the - # message in topological order of the dependency graph. - des_pool = pool or descriptor_pool.DescriptorPool() - file_by_name = {file_proto.name: file_proto for file_proto in file_protos} - def _AddFile(file_proto): - for dependency in file_proto.dependency: - if dependency in file_by_name: - # Remove from elements to be visited, in order to cut cycles. - _AddFile(file_by_name.pop(dependency)) - des_pool.Add(file_proto) - while file_by_name: - _AddFile(file_by_name.popitem()[1]) - return GetMessageClassesForFiles( - [file_proto.name for file_proto in file_protos], des_pool) diff --git a/script.module.protobuf/resources/lib/google/protobuf/proto_builder.py b/script.module.protobuf/resources/lib/google/protobuf/proto_builder.py deleted file mode 100644 index 8dab8b3ee..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/proto_builder.py +++ /dev/null @@ -1,134 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Dynamic Protobuf class creator.""" - -from collections import OrderedDict -import hashlib -import os - -from google.protobuf import descriptor_pb2 -from google.protobuf import descriptor -from google.protobuf import descriptor_pool -from google.protobuf import message_factory - - -def _GetMessageFromFactory(pool, full_name): - """Get a proto class from the MessageFactory by name. - - Args: - pool: a descriptor pool. - full_name: str, the fully qualified name of the proto type. - Returns: - A class, for the type identified by full_name. - Raises: - KeyError, if the proto is not found in the factory's descriptor pool. - """ - proto_descriptor = pool.FindMessageTypeByName(full_name) - proto_cls = message_factory.GetMessageClass(proto_descriptor) - return proto_cls - - -def MakeSimpleProtoClass(fields, full_name=None, pool=None): - """Create a Protobuf class whose fields are basic types. - - Note: this doesn't validate field names! - - Args: - fields: dict of {name: field_type} mappings for each field in the proto. If - this is an OrderedDict the order will be maintained, otherwise the - fields will be sorted by name. - full_name: optional str, the fully-qualified name of the proto type. - pool: optional DescriptorPool instance. - Returns: - a class, the new protobuf class with a FileDescriptor. - """ - pool_instance = pool or descriptor_pool.DescriptorPool() - if full_name is not None: - try: - proto_cls = _GetMessageFromFactory(pool_instance, full_name) - return proto_cls - except KeyError: - # The factory's DescriptorPool doesn't know about this class yet. - pass - - # Get a list of (name, field_type) tuples from the fields dict. If fields was - # an OrderedDict we keep the order, but otherwise we sort the field to ensure - # consistent ordering. - field_items = fields.items() - if not isinstance(fields, OrderedDict): - field_items = sorted(field_items) - - # Use a consistent file name that is unlikely to conflict with any imported - # proto files. - fields_hash = hashlib.sha1() - for f_name, f_type in field_items: - fields_hash.update(f_name.encode('utf-8')) - fields_hash.update(str(f_type).encode('utf-8')) - proto_file_name = fields_hash.hexdigest() + '.proto' - - # If the proto is anonymous, use the same hash to name it. - if full_name is None: - full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' + - fields_hash.hexdigest()) - try: - proto_cls = _GetMessageFromFactory(pool_instance, full_name) - return proto_cls - except KeyError: - # The factory's DescriptorPool doesn't know about this class yet. - pass - - # This is the first time we see this proto: add a new descriptor to the pool. - pool_instance.Add( - _MakeFileDescriptorProto(proto_file_name, full_name, field_items)) - return _GetMessageFromFactory(pool_instance, full_name) - - -def _MakeFileDescriptorProto(proto_file_name, full_name, field_items): - """Populate FileDescriptorProto for MessageFactory's DescriptorPool.""" - package, name = full_name.rsplit('.', 1) - file_proto = descriptor_pb2.FileDescriptorProto() - file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name) - file_proto.package = package - desc_proto = file_proto.message_type.add() - desc_proto.name = name - for f_number, (f_name, f_type) in enumerate(field_items, 1): - field_proto = desc_proto.field.add() - field_proto.name = f_name - # # If the number falls in the reserved range, reassign it to the correct - # # number after the range. - if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER: - f_number += ( - descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER - - descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1) - field_proto.number = f_number - field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL - field_proto.type = f_type - return file_proto diff --git a/script.module.protobuf/resources/lib/google/protobuf/pyext/__init__.py b/script.module.protobuf/resources/lib/google/protobuf/pyext/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.module.protobuf/resources/lib/google/protobuf/pyext/cpp_message.py b/script.module.protobuf/resources/lib/google/protobuf/pyext/cpp_message.py deleted file mode 100644 index ca290299f..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/pyext/cpp_message.py +++ /dev/null @@ -1,72 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Protocol message implementation hooks for C++ implementation. - -Contains helper functions used to create protocol message classes from -Descriptor objects at runtime backed by the protocol buffer C++ API. -""" - -__author__ = 'tibell@google.com (Johan Tibell)' - -from google.protobuf.internal import api_implementation - - -# pylint: disable=protected-access -_message = api_implementation._c_module -# TODO(jieluo): Remove this import after fix api_implementation -if _message is None: - from google.protobuf.pyext import _message - - -class GeneratedProtocolMessageType(_message.MessageMeta): - - """Metaclass for protocol message classes created at runtime from Descriptors. - - The protocol compiler currently uses this metaclass to create protocol - message classes at runtime. Clients can also manually create their own - classes at runtime, as in this example: - - mydescriptor = Descriptor(.....) - factory = symbol_database.Default() - factory.pool.AddDescriptor(mydescriptor) - MyProtoClass = factory.GetPrototype(mydescriptor) - myproto_instance = MyProtoClass() - myproto.foo_field = 23 - ... - - The above example will not work for nested types. If you wish to include them, - use reflection.MakeClass() instead of manually instantiating the class in - order to create the appropriate class structure. - """ - - # Must be consistent with the protocol-compiler code in - # proto2/compiler/internal/generator.*. - _DESCRIPTOR_KEY = 'DESCRIPTOR' diff --git a/script.module.protobuf/resources/lib/google/protobuf/reflection.py b/script.module.protobuf/resources/lib/google/protobuf/reflection.py deleted file mode 100644 index 1627669b9..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/reflection.py +++ /dev/null @@ -1,95 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# This code is meant to work on Python 2.4 and above only. - -"""Contains a metaclass and helper functions used to create -protocol message classes from Descriptor objects at runtime. - -Recall that a metaclass is the "type" of a class. -(A class is to a metaclass what an instance is to a class.) - -In this case, we use the GeneratedProtocolMessageType metaclass -to inject all the useful functionality into the classes -output by the protocol compiler at compile-time. - -The upshot of all this is that the real implementation -details for ALL pure-Python protocol buffers are *here in -this file*. -""" - -__author__ = 'robinson@google.com (Will Robinson)' - - -from google.protobuf import message_factory -from google.protobuf import symbol_database - -# The type of all Message classes. -# Part of the public interface, but normally only used by message factories. -GeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE - -MESSAGE_CLASS_CACHE = {} - - -# Deprecated. Please NEVER use reflection.ParseMessage(). -def ParseMessage(descriptor, byte_str): - """Generate a new Message instance from this Descriptor and a byte string. - - DEPRECATED: ParseMessage is deprecated because it is using MakeClass(). - Please use MessageFactory.GetPrototype() instead. - - Args: - descriptor: Protobuf Descriptor object - byte_str: Serialized protocol buffer byte string - - Returns: - Newly created protobuf Message object. - """ - result_class = MakeClass(descriptor) - new_msg = result_class() - new_msg.ParseFromString(byte_str) - return new_msg - - -# Deprecated. Please NEVER use reflection.MakeClass(). -def MakeClass(descriptor): - """Construct a class object for a protobuf described by descriptor. - - DEPRECATED: use MessageFactory.GetPrototype() instead. - - Args: - descriptor: A descriptor.Descriptor object describing the protobuf. - Returns: - The Message class object described by the descriptor. - """ - # Original implementation leads to duplicate message classes, which won't play - # well with extensions. Message factory info is also missing. - # Redirect to message_factory. - return message_factory.GetMessageClass(descriptor) diff --git a/script.module.protobuf/resources/lib/google/protobuf/service.py b/script.module.protobuf/resources/lib/google/protobuf/service.py deleted file mode 100644 index 562524632..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/service.py +++ /dev/null @@ -1,228 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""DEPRECATED: Declares the RPC service interfaces. - -This module declares the abstract interfaces underlying proto2 RPC -services. These are intended to be independent of any particular RPC -implementation, so that proto2 services can be used on top of a variety -of implementations. Starting with version 2.3.0, RPC implementations should -not try to build on these, but should instead provide code generator plugins -which generate code specific to the particular RPC implementation. This way -the generated code can be more appropriate for the implementation in use -and can avoid unnecessary layers of indirection. -""" - -__author__ = 'petar@google.com (Petar Petrov)' - - -class RpcException(Exception): - """Exception raised on failed blocking RPC method call.""" - pass - - -class Service(object): - - """Abstract base interface for protocol-buffer-based RPC services. - - Services themselves are abstract classes (implemented either by servers or as - stubs), but they subclass this base interface. The methods of this - interface can be used to call the methods of the service without knowing - its exact type at compile time (analogous to the Message interface). - """ - - def GetDescriptor(): - """Retrieves this service's descriptor.""" - raise NotImplementedError - - def CallMethod(self, method_descriptor, rpc_controller, - request, done): - """Calls a method of the service specified by method_descriptor. - - If "done" is None then the call is blocking and the response - message will be returned directly. Otherwise the call is asynchronous - and "done" will later be called with the response value. - - In the blocking case, RpcException will be raised on error. - - Preconditions: - - * method_descriptor.service == GetDescriptor - * request is of the exact same classes as returned by - GetRequestClass(method). - * After the call has started, the request must not be modified. - * "rpc_controller" is of the correct type for the RPC implementation being - used by this Service. For stubs, the "correct type" depends on the - RpcChannel which the stub is using. - - Postconditions: - - * "done" will be called when the method is complete. This may be - before CallMethod() returns or it may be at some point in the future. - * If the RPC failed, the response value passed to "done" will be None. - Further details about the failure can be found by querying the - RpcController. - """ - raise NotImplementedError - - def GetRequestClass(self, method_descriptor): - """Returns the class of the request message for the specified method. - - CallMethod() requires that the request is of a particular subclass of - Message. GetRequestClass() gets the default instance of this required - type. - - Example: - method = service.GetDescriptor().FindMethodByName("Foo") - request = stub.GetRequestClass(method)() - request.ParseFromString(input) - service.CallMethod(method, request, callback) - """ - raise NotImplementedError - - def GetResponseClass(self, method_descriptor): - """Returns the class of the response message for the specified method. - - This method isn't really needed, as the RpcChannel's CallMethod constructs - the response protocol message. It's provided anyway in case it is useful - for the caller to know the response type in advance. - """ - raise NotImplementedError - - -class RpcController(object): - - """An RpcController mediates a single method call. - - The primary purpose of the controller is to provide a way to manipulate - settings specific to the RPC implementation and to find out about RPC-level - errors. The methods provided by the RpcController interface are intended - to be a "least common denominator" set of features which we expect all - implementations to support. Specific implementations may provide more - advanced features (e.g. deadline propagation). - """ - - # Client-side methods below - - def Reset(self): - """Resets the RpcController to its initial state. - - After the RpcController has been reset, it may be reused in - a new call. Must not be called while an RPC is in progress. - """ - raise NotImplementedError - - def Failed(self): - """Returns true if the call failed. - - After a call has finished, returns true if the call failed. The possible - reasons for failure depend on the RPC implementation. Failed() must not - be called before a call has finished. If Failed() returns true, the - contents of the response message are undefined. - """ - raise NotImplementedError - - def ErrorText(self): - """If Failed is true, returns a human-readable description of the error.""" - raise NotImplementedError - - def StartCancel(self): - """Initiate cancellation. - - Advises the RPC system that the caller desires that the RPC call be - canceled. The RPC system may cancel it immediately, may wait awhile and - then cancel it, or may not even cancel the call at all. If the call is - canceled, the "done" callback will still be called and the RpcController - will indicate that the call failed at that time. - """ - raise NotImplementedError - - # Server-side methods below - - def SetFailed(self, reason): - """Sets a failure reason. - - Causes Failed() to return true on the client side. "reason" will be - incorporated into the message returned by ErrorText(). If you find - you need to return machine-readable information about failures, you - should incorporate it into your response protocol buffer and should - NOT call SetFailed(). - """ - raise NotImplementedError - - def IsCanceled(self): - """Checks if the client cancelled the RPC. - - If true, indicates that the client canceled the RPC, so the server may - as well give up on replying to it. The server should still call the - final "done" callback. - """ - raise NotImplementedError - - def NotifyOnCancel(self, callback): - """Sets a callback to invoke on cancel. - - Asks that the given callback be called when the RPC is canceled. The - callback will always be called exactly once. If the RPC completes without - being canceled, the callback will be called after completion. If the RPC - has already been canceled when NotifyOnCancel() is called, the callback - will be called immediately. - - NotifyOnCancel() must be called no more than once per request. - """ - raise NotImplementedError - - -class RpcChannel(object): - - """Abstract interface for an RPC channel. - - An RpcChannel represents a communication line to a service which can be used - to call that service's methods. The service may be running on another - machine. Normally, you should not use an RpcChannel directly, but instead - construct a stub {@link Service} wrapping it. Example: - - Example: - RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234") - RpcController controller = rpcImpl.Controller() - MyService service = MyService_Stub(channel) - service.MyMethod(controller, request, callback) - """ - - def CallMethod(self, method_descriptor, rpc_controller, - request, response_class, done): - """Calls the method identified by the descriptor. - - Call the given method of the remote service. The signature of this - procedure looks the same as Service.CallMethod(), but the requirements - are less strict in one important way: the request object doesn't have to - be of any specific class as long as its descriptor is method.input_type. - """ - raise NotImplementedError diff --git a/script.module.protobuf/resources/lib/google/protobuf/service_reflection.py b/script.module.protobuf/resources/lib/google/protobuf/service_reflection.py deleted file mode 100644 index f82ab7145..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/service_reflection.py +++ /dev/null @@ -1,295 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains metaclasses used to create protocol service and service stub -classes from ServiceDescriptor objects at runtime. - -The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to -inject all useful functionality into the classes output by the protocol -compiler at compile-time. -""" - -__author__ = 'petar@google.com (Petar Petrov)' - - -class GeneratedServiceType(type): - - """Metaclass for service classes created at runtime from ServiceDescriptors. - - Implementations for all methods described in the Service class are added here - by this class. We also create properties to allow getting/setting all fields - in the protocol message. - - The protocol compiler currently uses this metaclass to create protocol service - classes at runtime. Clients can also manually create their own classes at - runtime, as in this example:: - - mydescriptor = ServiceDescriptor(.....) - class MyProtoService(service.Service): - __metaclass__ = GeneratedServiceType - DESCRIPTOR = mydescriptor - myservice_instance = MyProtoService() - # ... - """ - - _DESCRIPTOR_KEY = 'DESCRIPTOR' - - def __init__(cls, name, bases, dictionary): - """Creates a message service class. - - Args: - name: Name of the class (ignored, but required by the metaclass - protocol). - bases: Base classes of the class being constructed. - dictionary: The class dictionary of the class being constructed. - dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object - describing this protocol service type. - """ - # Don't do anything if this class doesn't have a descriptor. This happens - # when a service class is subclassed. - if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary: - return - - descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY] - service_builder = _ServiceBuilder(descriptor) - service_builder.BuildService(cls) - cls.DESCRIPTOR = descriptor - - -class GeneratedServiceStubType(GeneratedServiceType): - - """Metaclass for service stubs created at runtime from ServiceDescriptors. - - This class has similar responsibilities as GeneratedServiceType, except that - it creates the service stub classes. - """ - - _DESCRIPTOR_KEY = 'DESCRIPTOR' - - def __init__(cls, name, bases, dictionary): - """Creates a message service stub class. - - Args: - name: Name of the class (ignored, here). - bases: Base classes of the class being constructed. - dictionary: The class dictionary of the class being constructed. - dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object - describing this protocol service type. - """ - super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary) - # Don't do anything if this class doesn't have a descriptor. This happens - # when a service stub is subclassed. - if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary: - return - - descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY] - service_stub_builder = _ServiceStubBuilder(descriptor) - service_stub_builder.BuildServiceStub(cls) - - -class _ServiceBuilder(object): - - """This class constructs a protocol service class using a service descriptor. - - Given a service descriptor, this class constructs a class that represents - the specified service descriptor. One service builder instance constructs - exactly one service class. That means all instances of that class share the - same builder. - """ - - def __init__(self, service_descriptor): - """Initializes an instance of the service class builder. - - Args: - service_descriptor: ServiceDescriptor to use when constructing the - service class. - """ - self.descriptor = service_descriptor - - def BuildService(builder, cls): - """Constructs the service class. - - Args: - cls: The class that will be constructed. - """ - - # CallMethod needs to operate with an instance of the Service class. This - # internal wrapper function exists only to be able to pass the service - # instance to the method that does the real CallMethod work. - # Making sure to use exact argument names from the abstract interface in - # service.py to match the type signature - def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done): - return builder._CallMethod(self, method_descriptor, rpc_controller, - request, done) - - def _WrapGetRequestClass(self, method_descriptor): - return builder._GetRequestClass(method_descriptor) - - def _WrapGetResponseClass(self, method_descriptor): - return builder._GetResponseClass(method_descriptor) - - builder.cls = cls - cls.CallMethod = _WrapCallMethod - cls.GetDescriptor = staticmethod(lambda: builder.descriptor) - cls.GetDescriptor.__doc__ = 'Returns the service descriptor.' - cls.GetRequestClass = _WrapGetRequestClass - cls.GetResponseClass = _WrapGetResponseClass - for method in builder.descriptor.methods: - setattr(cls, method.name, builder._GenerateNonImplementedMethod(method)) - - def _CallMethod(self, srvc, method_descriptor, - rpc_controller, request, callback): - """Calls the method described by a given method descriptor. - - Args: - srvc: Instance of the service for which this method is called. - method_descriptor: Descriptor that represent the method to call. - rpc_controller: RPC controller to use for this method's execution. - request: Request protocol message. - callback: A callback to invoke after the method has completed. - """ - if method_descriptor.containing_service != self.descriptor: - raise RuntimeError( - 'CallMethod() given method descriptor for wrong service type.') - method = getattr(srvc, method_descriptor.name) - return method(rpc_controller, request, callback) - - def _GetRequestClass(self, method_descriptor): - """Returns the class of the request protocol message. - - Args: - method_descriptor: Descriptor of the method for which to return the - request protocol message class. - - Returns: - A class that represents the input protocol message of the specified - method. - """ - if method_descriptor.containing_service != self.descriptor: - raise RuntimeError( - 'GetRequestClass() given method descriptor for wrong service type.') - return method_descriptor.input_type._concrete_class - - def _GetResponseClass(self, method_descriptor): - """Returns the class of the response protocol message. - - Args: - method_descriptor: Descriptor of the method for which to return the - response protocol message class. - - Returns: - A class that represents the output protocol message of the specified - method. - """ - if method_descriptor.containing_service != self.descriptor: - raise RuntimeError( - 'GetResponseClass() given method descriptor for wrong service type.') - return method_descriptor.output_type._concrete_class - - def _GenerateNonImplementedMethod(self, method): - """Generates and returns a method that can be set for a service methods. - - Args: - method: Descriptor of the service method for which a method is to be - generated. - - Returns: - A method that can be added to the service class. - """ - return lambda inst, rpc_controller, request, callback: ( - self._NonImplementedMethod(method.name, rpc_controller, callback)) - - def _NonImplementedMethod(self, method_name, rpc_controller, callback): - """The body of all methods in the generated service class. - - Args: - method_name: Name of the method being executed. - rpc_controller: RPC controller used to execute this method. - callback: A callback which will be invoked when the method finishes. - """ - rpc_controller.SetFailed('Method %s not implemented.' % method_name) - callback(None) - - -class _ServiceStubBuilder(object): - - """Constructs a protocol service stub class using a service descriptor. - - Given a service descriptor, this class constructs a suitable stub class. - A stub is just a type-safe wrapper around an RpcChannel which emulates a - local implementation of the service. - - One service stub builder instance constructs exactly one class. It means all - instances of that class share the same service stub builder. - """ - - def __init__(self, service_descriptor): - """Initializes an instance of the service stub class builder. - - Args: - service_descriptor: ServiceDescriptor to use when constructing the - stub class. - """ - self.descriptor = service_descriptor - - def BuildServiceStub(self, cls): - """Constructs the stub class. - - Args: - cls: The class that will be constructed. - """ - - def _ServiceStubInit(stub, rpc_channel): - stub.rpc_channel = rpc_channel - self.cls = cls - cls.__init__ = _ServiceStubInit - for method in self.descriptor.methods: - setattr(cls, method.name, self._GenerateStubMethod(method)) - - def _GenerateStubMethod(self, method): - return (lambda inst, rpc_controller, request, callback=None: - self._StubMethod(inst, method, rpc_controller, request, callback)) - - def _StubMethod(self, stub, method_descriptor, - rpc_controller, request, callback): - """The body of all service methods in the generated stub class. - - Args: - stub: Stub instance. - method_descriptor: Descriptor of the invoked method. - rpc_controller: Rpc controller to execute the method. - request: Request protocol message. - callback: A callback to execute when the method finishes. - Returns: - Response message (in case of blocking call). - """ - return stub.rpc_channel.CallMethod( - method_descriptor, rpc_controller, request, - method_descriptor.output_type._concrete_class, callback) diff --git a/script.module.protobuf/resources/lib/google/protobuf/source_context_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/source_context_pb2.py deleted file mode 100644 index e69cd1759..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/source_context_pb2.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/source_context.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$google/protobuf/source_context.proto\x12\x0fgoogle.protobuf\",\n\rSourceContext\x12\x1b\n\tfile_name\x18\x01 \x01(\tR\x08\x66ileNameB\x8a\x01\n\x13\x63om.google.protobufB\x12SourceContextProtoP\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\022SourceContextProtoP\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_SOURCECONTEXT']._serialized_start=57 - _globals['_SOURCECONTEXT']._serialized_end=101 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/struct_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/struct_pb2.py deleted file mode 100644 index ec1d62d04..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/struct_pb2.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/struct.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cgoogle/protobuf/struct.proto\x12\x0fgoogle.protobuf\"\x98\x01\n\x06Struct\x12;\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.google.protobuf.Struct.FieldsEntryR\x06\x66ields\x1aQ\n\x0b\x46ieldsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x05value:\x02\x38\x01\"\xb2\x02\n\x05Value\x12;\n\nnull_value\x18\x01 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00R\tnullValue\x12#\n\x0cnumber_value\x18\x02 \x01(\x01H\x00R\x0bnumberValue\x12#\n\x0cstring_value\x18\x03 \x01(\tH\x00R\x0bstringValue\x12\x1f\n\nbool_value\x18\x04 \x01(\x08H\x00R\tboolValue\x12<\n\x0cstruct_value\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x00R\x0bstructValue\x12;\n\nlist_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x00R\tlistValueB\x06\n\x04kind\";\n\tListValue\x12.\n\x06values\x18\x01 \x03(\x0b\x32\x16.google.protobuf.ValueR\x06values*\x1b\n\tNullValue\x12\x0e\n\nNULL_VALUE\x10\x00\x42\x7f\n\x13\x63om.google.protobufB\x0bStructProtoP\x01Z/google.golang.org/protobuf/types/known/structpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\013StructProtoP\001Z/google.golang.org/protobuf/types/known/structpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _STRUCT_FIELDSENTRY._options = None - _STRUCT_FIELDSENTRY._serialized_options = b'8\001' - _globals['_NULLVALUE']._serialized_start=574 - _globals['_NULLVALUE']._serialized_end=601 - _globals['_STRUCT']._serialized_start=50 - _globals['_STRUCT']._serialized_end=202 - _globals['_STRUCT_FIELDSENTRY']._serialized_start=121 - _globals['_STRUCT_FIELDSENTRY']._serialized_end=202 - _globals['_VALUE']._serialized_start=205 - _globals['_VALUE']._serialized_end=511 - _globals['_LISTVALUE']._serialized_start=513 - _globals['_LISTVALUE']._serialized_end=572 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/symbol_database.py b/script.module.protobuf/resources/lib/google/protobuf/symbol_database.py deleted file mode 100644 index 390c49810..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/symbol_database.py +++ /dev/null @@ -1,220 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""A database of Python protocol buffer generated symbols. - -SymbolDatabase is the MessageFactory for messages generated at compile time, -and makes it easy to create new instances of a registered type, given only the -type's protocol buffer symbol name. - -Example usage:: - - db = symbol_database.SymbolDatabase() - - # Register symbols of interest, from one or multiple files. - db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR) - db.RegisterMessage(my_proto_pb2.MyMessage) - db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR) - - # The database can be used as a MessageFactory, to generate types based on - # their name: - types = db.GetMessages(['my_proto.proto']) - my_message_instance = types['MyMessage']() - - # The database's underlying descriptor pool can be queried, so it's not - # necessary to know a type's filename to be able to generate it: - filename = db.pool.FindFileContainingSymbol('MyMessage') - my_message_instance = db.GetMessages([filename])['MyMessage']() - - # This functionality is also provided directly via a convenience method: - my_message_instance = db.GetSymbol('MyMessage')() -""" - -import warnings - -from google.protobuf.internal import api_implementation -from google.protobuf import descriptor_pool -from google.protobuf import message_factory - - -class SymbolDatabase(): - """A database of Python generated symbols.""" - - # local cache of registered classes. - _classes = {} - - def __init__(self, pool=None): - """Initializes a new SymbolDatabase.""" - self.pool = pool or descriptor_pool.DescriptorPool() - - def GetPrototype(self, descriptor): - warnings.warn('SymbolDatabase.GetPrototype() is deprecated. Please ' - 'use message_factory.GetMessageClass() instead. ' - 'SymbolDatabase.GetPrototype() will be removed soon.') - return message_factory.GetMessageClass(descriptor) - - def CreatePrototype(self, descriptor): - warnings.warn('Directly call CreatePrototype() is wrong. Please use ' - 'message_factory.GetMessageClass() instead. ' - 'SymbolDatabase.CreatePrototype() will be removed soon.') - return message_factory._InternalCreateMessageClass(descriptor) - - def GetMessages(self, files): - warnings.warn('SymbolDatabase.GetMessages() is deprecated. Please use ' - 'message_factory.GetMessageClassedForFiles() instead. ' - 'SymbolDatabase.GetMessages() will be removed soon.') - return message_factory.GetMessageClassedForFiles(files, self.pool) - - def RegisterMessage(self, message): - """Registers the given message type in the local database. - - Calls to GetSymbol() and GetMessages() will return messages registered here. - - Args: - message: A :class:`google.protobuf.message.Message` subclass (or - instance); its descriptor will be registered. - - Returns: - The provided message. - """ - - desc = message.DESCRIPTOR - self._classes[desc] = message - self.RegisterMessageDescriptor(desc) - return message - - def RegisterMessageDescriptor(self, message_descriptor): - """Registers the given message descriptor in the local database. - - Args: - message_descriptor (Descriptor): the message descriptor to add. - """ - if api_implementation.Type() == 'python': - # pylint: disable=protected-access - self.pool._AddDescriptor(message_descriptor) - - def RegisterEnumDescriptor(self, enum_descriptor): - """Registers the given enum descriptor in the local database. - - Args: - enum_descriptor (EnumDescriptor): The enum descriptor to register. - - Returns: - EnumDescriptor: The provided descriptor. - """ - if api_implementation.Type() == 'python': - # pylint: disable=protected-access - self.pool._AddEnumDescriptor(enum_descriptor) - return enum_descriptor - - def RegisterServiceDescriptor(self, service_descriptor): - """Registers the given service descriptor in the local database. - - Args: - service_descriptor (ServiceDescriptor): the service descriptor to - register. - """ - if api_implementation.Type() == 'python': - # pylint: disable=protected-access - self.pool._AddServiceDescriptor(service_descriptor) - - def RegisterFileDescriptor(self, file_descriptor): - """Registers the given file descriptor in the local database. - - Args: - file_descriptor (FileDescriptor): The file descriptor to register. - """ - if api_implementation.Type() == 'python': - # pylint: disable=protected-access - self.pool._InternalAddFileDescriptor(file_descriptor) - - def GetSymbol(self, symbol): - """Tries to find a symbol in the local database. - - Currently, this method only returns message.Message instances, however, if - may be extended in future to support other symbol types. - - Args: - symbol (str): a protocol buffer symbol. - - Returns: - A Python class corresponding to the symbol. - - Raises: - KeyError: if the symbol could not be found. - """ - - return self._classes[self.pool.FindMessageTypeByName(symbol)] - - def GetMessages(self, files): - # TODO(amauryfa): Fix the differences with MessageFactory. - """Gets all registered messages from a specified file. - - Only messages already created and registered will be returned; (this is the - case for imported _pb2 modules) - But unlike MessageFactory, this version also returns already defined nested - messages, but does not register any message extensions. - - Args: - files (list[str]): The file names to extract messages from. - - Returns: - A dictionary mapping proto names to the message classes. - - Raises: - KeyError: if a file could not be found. - """ - - def _GetAllMessages(desc): - """Walk a message Descriptor and recursively yields all message names.""" - yield desc - for msg_desc in desc.nested_types: - for nested_desc in _GetAllMessages(msg_desc): - yield nested_desc - - result = {} - for file_name in files: - file_desc = self.pool.FindFileByName(file_name) - for msg_desc in file_desc.message_types_by_name.values(): - for desc in _GetAllMessages(msg_desc): - try: - result[desc.full_name] = self._classes[desc] - except KeyError: - # This descriptor has no registered class, skip it. - pass - return result - - -_DEFAULT = SymbolDatabase(pool=descriptor_pool.Default()) - - -def Default(): - """Returns the default SymbolDatabase.""" - return _DEFAULT diff --git a/script.module.protobuf/resources/lib/google/protobuf/text_encoding.py b/script.module.protobuf/resources/lib/google/protobuf/text_encoding.py deleted file mode 100644 index 1955b6a3c..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/text_encoding.py +++ /dev/null @@ -1,108 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Encoding related utilities.""" -import re - -_cescape_chr_to_symbol_map = {} -_cescape_chr_to_symbol_map[9] = r'\t' # optional escape -_cescape_chr_to_symbol_map[10] = r'\n' # optional escape -_cescape_chr_to_symbol_map[13] = r'\r' # optional escape -_cescape_chr_to_symbol_map[34] = r'\"' # necessary escape -_cescape_chr_to_symbol_map[39] = r"\'" # optional escape -_cescape_chr_to_symbol_map[92] = r'\\' # necessary escape - -# Lookup table for unicode -_cescape_unicode_to_str = [chr(i) for i in range(0, 256)] -for byte, string in _cescape_chr_to_symbol_map.items(): - _cescape_unicode_to_str[byte] = string - -# Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32) -_cescape_byte_to_str = ([r'\%03o' % i for i in range(0, 32)] + - [chr(i) for i in range(32, 127)] + - [r'\%03o' % i for i in range(127, 256)]) -for byte, string in _cescape_chr_to_symbol_map.items(): - _cescape_byte_to_str[byte] = string -del byte, string - - -def CEscape(text, as_utf8) -> str: - """Escape a bytes string for use in an text protocol buffer. - - Args: - text: A byte string to be escaped. - as_utf8: Specifies if result may contain non-ASCII characters. - In Python 3 this allows unescaped non-ASCII Unicode characters. - In Python 2 the return value will be valid UTF-8 rather than only ASCII. - Returns: - Escaped string (str). - """ - # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not - # satisfy our needs; they encodes unprintable characters using two-digit hex - # escapes whereas our C++ unescaping function allows hex escapes to be any - # length. So, "\0011".encode('string_escape') ends up being "\\x011", which - # will be decoded in C++ as a single-character string with char code 0x11. - text_is_unicode = isinstance(text, str) - if as_utf8 and text_is_unicode: - # We're already unicode, no processing beyond control char escapes. - return text.translate(_cescape_chr_to_symbol_map) - ord_ = ord if text_is_unicode else lambda x: x # bytes iterate as ints. - if as_utf8: - return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text) - return ''.join(_cescape_byte_to_str[ord_(c)] for c in text) - - -_CUNESCAPE_HEX = re.compile(r'(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])') - - -def CUnescape(text: str) -> bytes: - """Unescape a text string with C-style escape sequences to UTF-8 bytes. - - Args: - text: The data to parse in a str. - Returns: - A byte string. - """ - - def ReplaceHex(m): - # Only replace the match if the number of leading back slashes is odd. i.e. - # the slash itself is not escaped. - if len(m.group(1)) & 1: - return m.group(1) + 'x0' + m.group(2) - return m.group(0) - - # This is required because the 'string_escape' encoding doesn't - # allow single-digit hex escapes (like '\xf'). - result = _CUNESCAPE_HEX.sub(ReplaceHex, text) - - return (result.encode('utf-8') # Make it bytes to allow decode. - .decode('unicode_escape') - # Make it bytes again to return the proper type. - .encode('raw_unicode_escape')) diff --git a/script.module.protobuf/resources/lib/google/protobuf/text_format.py b/script.module.protobuf/resources/lib/google/protobuf/text_format.py deleted file mode 100644 index 7a6ca7df0..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/text_format.py +++ /dev/null @@ -1,1857 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains routines for printing protocol messages in text format. - -Simple usage example:: - - # Create a proto object and serialize it to a text proto string. - message = my_proto_pb2.MyMessage(foo='bar') - text_proto = text_format.MessageToString(message) - - # Parse a text proto string. - message = text_format.Parse(text_proto, my_proto_pb2.MyMessage()) -""" - -__author__ = 'kenton@google.com (Kenton Varda)' - -# TODO(b/129989314) Import thread contention leads to test failures. -import encodings.raw_unicode_escape # pylint: disable=unused-import -import encodings.unicode_escape # pylint: disable=unused-import -import io -import math -import re - -from google.protobuf.internal import decoder -from google.protobuf.internal import type_checkers -from google.protobuf import descriptor -from google.protobuf import text_encoding -from google.protobuf import unknown_fields - -# pylint: disable=g-import-not-at-top -__all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField', - 'PrintFieldValue', 'Merge', 'MessageToBytes'] - -_INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(), - type_checkers.Int32ValueChecker(), - type_checkers.Uint64ValueChecker(), - type_checkers.Int64ValueChecker()) -_FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE) -_FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE) -_QUOTES = frozenset(("'", '"')) -_ANY_FULL_TYPE_NAME = 'google.protobuf.Any' -_DEBUG_STRING_SILENT_MARKER = '\t ' - - -class Error(Exception): - """Top-level module error for text_format.""" - - -class ParseError(Error): - """Thrown in case of text parsing or tokenizing error.""" - - def __init__(self, message=None, line=None, column=None): - if message is not None and line is not None: - loc = str(line) - if column is not None: - loc += ':{0}'.format(column) - message = '{0} : {1}'.format(loc, message) - if message is not None: - super(ParseError, self).__init__(message) - else: - super(ParseError, self).__init__() - self._line = line - self._column = column - - def GetLine(self): - return self._line - - def GetColumn(self): - return self._column - - -class TextWriter(object): - - def __init__(self, as_utf8): - self._writer = io.StringIO() - - def write(self, val): - return self._writer.write(val) - - def close(self): - return self._writer.close() - - def getvalue(self): - return self._writer.getvalue() - - -def MessageToString( - message, - as_utf8=False, - as_one_line=False, - use_short_repeated_primitives=False, - pointy_brackets=False, - use_index_order=False, - float_format=None, - double_format=None, - use_field_number=False, - descriptor_pool=None, - indent=0, - message_formatter=None, - print_unknown_fields=False, - force_colon=False) -> str: - """Convert protobuf message to text format. - - Double values can be formatted compactly with 15 digits of - precision (which is the most that IEEE 754 "double" can guarantee) - using double_format='.15g'. To ensure that converting to text and back to a - proto will result in an identical value, double_format='.17g' should be used. - - Args: - message: The protocol buffers message. - as_utf8: Return unescaped Unicode for non-ASCII characters. - as_one_line: Don't introduce newlines between fields. - use_short_repeated_primitives: Use short repeated format for primitives. - pointy_brackets: If True, use angle brackets instead of curly braces for - nesting. - use_index_order: If True, fields of a proto message will be printed using - the order defined in source code instead of the field number, extensions - will be printed at the end of the message and their relative order is - determined by the extension number. By default, use the field number - order. - float_format (str): If set, use this to specify float field formatting - (per the "Format Specification Mini-Language"); otherwise, shortest float - that has same value in wire will be printed. Also affect double field - if double_format is not set but float_format is set. - double_format (str): If set, use this to specify double field formatting - (per the "Format Specification Mini-Language"); if it is not set but - float_format is set, use float_format. Otherwise, use ``str()`` - use_field_number: If True, print field numbers instead of names. - descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. - indent (int): The initial indent level, in terms of spaces, for pretty - print. - message_formatter (function(message, indent, as_one_line) -> unicode|None): - Custom formatter for selected sub-messages (usually based on message - type). Use to pretty print parts of the protobuf for easier diffing. - print_unknown_fields: If True, unknown fields will be printed. - force_colon: If set, a colon will be added after the field name even if the - field is a proto message. - - Returns: - str: A string of the text formatted protocol buffer message. - """ - out = TextWriter(as_utf8) - printer = _Printer( - out, - indent, - as_utf8, - as_one_line, - use_short_repeated_primitives, - pointy_brackets, - use_index_order, - float_format, - double_format, - use_field_number, - descriptor_pool, - message_formatter, - print_unknown_fields=print_unknown_fields, - force_colon=force_colon) - printer.PrintMessage(message) - result = out.getvalue() - out.close() - if as_one_line: - return result.rstrip() - return result - - -def MessageToBytes(message, **kwargs) -> bytes: - """Convert protobuf message to encoded text format. See MessageToString.""" - text = MessageToString(message, **kwargs) - if isinstance(text, bytes): - return text - codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii' - return text.encode(codec) - - -def _IsMapEntry(field): - return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and - field.message_type.has_options and - field.message_type.GetOptions().map_entry) - - -def PrintMessage(message, - out, - indent=0, - as_utf8=False, - as_one_line=False, - use_short_repeated_primitives=False, - pointy_brackets=False, - use_index_order=False, - float_format=None, - double_format=None, - use_field_number=False, - descriptor_pool=None, - message_formatter=None, - print_unknown_fields=False, - force_colon=False): - """Convert the message to text format and write it to the out stream. - - Args: - message: The Message object to convert to text format. - out: A file handle to write the message to. - indent: The initial indent level for pretty print. - as_utf8: Return unescaped Unicode for non-ASCII characters. - as_one_line: Don't introduce newlines between fields. - use_short_repeated_primitives: Use short repeated format for primitives. - pointy_brackets: If True, use angle brackets instead of curly braces for - nesting. - use_index_order: If True, print fields of a proto message using the order - defined in source code instead of the field number. By default, use the - field number order. - float_format: If set, use this to specify float field formatting - (per the "Format Specification Mini-Language"); otherwise, shortest - float that has same value in wire will be printed. Also affect double - field if double_format is not set but float_format is set. - double_format: If set, use this to specify double field formatting - (per the "Format Specification Mini-Language"); if it is not set but - float_format is set, use float_format. Otherwise, str() is used. - use_field_number: If True, print field numbers instead of names. - descriptor_pool: A DescriptorPool used to resolve Any types. - message_formatter: A function(message, indent, as_one_line): unicode|None - to custom format selected sub-messages (usually based on message type). - Use to pretty print parts of the protobuf for easier diffing. - print_unknown_fields: If True, unknown fields will be printed. - force_colon: If set, a colon will be added after the field name even if - the field is a proto message. - """ - printer = _Printer( - out=out, indent=indent, as_utf8=as_utf8, - as_one_line=as_one_line, - use_short_repeated_primitives=use_short_repeated_primitives, - pointy_brackets=pointy_brackets, - use_index_order=use_index_order, - float_format=float_format, - double_format=double_format, - use_field_number=use_field_number, - descriptor_pool=descriptor_pool, - message_formatter=message_formatter, - print_unknown_fields=print_unknown_fields, - force_colon=force_colon) - printer.PrintMessage(message) - - -def PrintField(field, - value, - out, - indent=0, - as_utf8=False, - as_one_line=False, - use_short_repeated_primitives=False, - pointy_brackets=False, - use_index_order=False, - float_format=None, - double_format=None, - message_formatter=None, - print_unknown_fields=False, - force_colon=False): - """Print a single field name/value pair.""" - printer = _Printer(out, indent, as_utf8, as_one_line, - use_short_repeated_primitives, pointy_brackets, - use_index_order, float_format, double_format, - message_formatter=message_formatter, - print_unknown_fields=print_unknown_fields, - force_colon=force_colon) - printer.PrintField(field, value) - - -def PrintFieldValue(field, - value, - out, - indent=0, - as_utf8=False, - as_one_line=False, - use_short_repeated_primitives=False, - pointy_brackets=False, - use_index_order=False, - float_format=None, - double_format=None, - message_formatter=None, - print_unknown_fields=False, - force_colon=False): - """Print a single field value (not including name).""" - printer = _Printer(out, indent, as_utf8, as_one_line, - use_short_repeated_primitives, pointy_brackets, - use_index_order, float_format, double_format, - message_formatter=message_formatter, - print_unknown_fields=print_unknown_fields, - force_colon=force_colon) - printer.PrintFieldValue(field, value) - - -def _BuildMessageFromTypeName(type_name, descriptor_pool): - """Returns a protobuf message instance. - - Args: - type_name: Fully-qualified protobuf message type name string. - descriptor_pool: DescriptorPool instance. - - Returns: - A Message instance of type matching type_name, or None if the a Descriptor - wasn't found matching type_name. - """ - # pylint: disable=g-import-not-at-top - if descriptor_pool is None: - from google.protobuf import descriptor_pool as pool_mod - descriptor_pool = pool_mod.Default() - from google.protobuf import message_factory - try: - message_descriptor = descriptor_pool.FindMessageTypeByName(type_name) - except KeyError: - return None - message_type = message_factory.GetMessageClass(message_descriptor) - return message_type() - - -# These values must match WireType enum in //google/protobuf/wire_format.h. -WIRETYPE_LENGTH_DELIMITED = 2 -WIRETYPE_START_GROUP = 3 - - -class _Printer(object): - """Text format printer for protocol message.""" - - def __init__( - self, - out, - indent=0, - as_utf8=False, - as_one_line=False, - use_short_repeated_primitives=False, - pointy_brackets=False, - use_index_order=False, - float_format=None, - double_format=None, - use_field_number=False, - descriptor_pool=None, - message_formatter=None, - print_unknown_fields=False, - force_colon=False): - """Initialize the Printer. - - Double values can be formatted compactly with 15 digits of precision - (which is the most that IEEE 754 "double" can guarantee) using - double_format='.15g'. To ensure that converting to text and back to a proto - will result in an identical value, double_format='.17g' should be used. - - Args: - out: To record the text format result. - indent: The initial indent level for pretty print. - as_utf8: Return unescaped Unicode for non-ASCII characters. - as_one_line: Don't introduce newlines between fields. - use_short_repeated_primitives: Use short repeated format for primitives. - pointy_brackets: If True, use angle brackets instead of curly braces for - nesting. - use_index_order: If True, print fields of a proto message using the order - defined in source code instead of the field number. By default, use the - field number order. - float_format: If set, use this to specify float field formatting - (per the "Format Specification Mini-Language"); otherwise, shortest - float that has same value in wire will be printed. Also affect double - field if double_format is not set but float_format is set. - double_format: If set, use this to specify double field formatting - (per the "Format Specification Mini-Language"); if it is not set but - float_format is set, use float_format. Otherwise, str() is used. - use_field_number: If True, print field numbers instead of names. - descriptor_pool: A DescriptorPool used to resolve Any types. - message_formatter: A function(message, indent, as_one_line): unicode|None - to custom format selected sub-messages (usually based on message type). - Use to pretty print parts of the protobuf for easier diffing. - print_unknown_fields: If True, unknown fields will be printed. - force_colon: If set, a colon will be added after the field name even if - the field is a proto message. - """ - self.out = out - self.indent = indent - self.as_utf8 = as_utf8 - self.as_one_line = as_one_line - self.use_short_repeated_primitives = use_short_repeated_primitives - self.pointy_brackets = pointy_brackets - self.use_index_order = use_index_order - self.float_format = float_format - if double_format is not None: - self.double_format = double_format - else: - self.double_format = float_format - self.use_field_number = use_field_number - self.descriptor_pool = descriptor_pool - self.message_formatter = message_formatter - self.print_unknown_fields = print_unknown_fields - self.force_colon = force_colon - - def _TryPrintAsAnyMessage(self, message): - """Serializes if message is a google.protobuf.Any field.""" - if '/' not in message.type_url: - return False - packed_message = _BuildMessageFromTypeName(message.TypeName(), - self.descriptor_pool) - if packed_message: - packed_message.MergeFromString(message.value) - colon = ':' if self.force_colon else '' - self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon)) - self._PrintMessageFieldValue(packed_message) - self.out.write(' ' if self.as_one_line else '\n') - return True - else: - return False - - def _TryCustomFormatMessage(self, message): - formatted = self.message_formatter(message, self.indent, self.as_one_line) - if formatted is None: - return False - - out = self.out - out.write(' ' * self.indent) - out.write(formatted) - out.write(' ' if self.as_one_line else '\n') - return True - - def PrintMessage(self, message): - """Convert protobuf message to text format. - - Args: - message: The protocol buffers message. - """ - if self.message_formatter and self._TryCustomFormatMessage(message): - return - if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and - self._TryPrintAsAnyMessage(message)): - return - fields = message.ListFields() - if self.use_index_order: - fields.sort( - key=lambda x: x[0].number if x[0].is_extension else x[0].index) - for field, value in fields: - if _IsMapEntry(field): - for key in sorted(value): - # This is slow for maps with submessage entries because it copies the - # entire tree. Unfortunately this would take significant refactoring - # of this file to work around. - # - # TODO(haberman): refactor and optimize if this becomes an issue. - entry_submsg = value.GetEntryClass()(key=key, value=value[key]) - self.PrintField(field, entry_submsg) - elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: - if (self.use_short_repeated_primitives - and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE - and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING): - self._PrintShortRepeatedPrimitivesValue(field, value) - else: - for element in value: - self.PrintField(field, element) - else: - self.PrintField(field, value) - - if self.print_unknown_fields: - self._PrintUnknownFields(unknown_fields.UnknownFieldSet(message)) - - def _PrintUnknownFields(self, unknown_field_set): - """Print unknown fields.""" - out = self.out - for field in unknown_field_set: - out.write(' ' * self.indent) - out.write(str(field.field_number)) - if field.wire_type == WIRETYPE_START_GROUP: - if self.as_one_line: - out.write(' { ') - else: - out.write(' {\n') - self.indent += 2 - - self._PrintUnknownFields(field.data) - - if self.as_one_line: - out.write('} ') - else: - self.indent -= 2 - out.write(' ' * self.indent + '}\n') - elif field.wire_type == WIRETYPE_LENGTH_DELIMITED: - try: - # If this field is parseable as a Message, it is probably - # an embedded message. - # pylint: disable=protected-access - (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet( - memoryview(field.data), 0, len(field.data)) - except Exception: # pylint: disable=broad-except - pos = 0 - - if pos == len(field.data): - if self.as_one_line: - out.write(' { ') - else: - out.write(' {\n') - self.indent += 2 - - self._PrintUnknownFields(embedded_unknown_message) - - if self.as_one_line: - out.write('} ') - else: - self.indent -= 2 - out.write(' ' * self.indent + '}\n') - else: - # A string or bytes field. self.as_utf8 may not work. - out.write(': \"') - out.write(text_encoding.CEscape(field.data, False)) - out.write('\" ' if self.as_one_line else '\"\n') - else: - # varint, fixed32, fixed64 - out.write(': ') - out.write(str(field.data)) - out.write(' ' if self.as_one_line else '\n') - - def _PrintFieldName(self, field): - """Print field name.""" - out = self.out - out.write(' ' * self.indent) - if self.use_field_number: - out.write(str(field.number)) - else: - if field.is_extension: - out.write('[') - if (field.containing_type.GetOptions().message_set_wire_format and - field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and - field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL): - out.write(field.message_type.full_name) - else: - out.write(field.full_name) - out.write(']') - elif field.type == descriptor.FieldDescriptor.TYPE_GROUP: - # For groups, use the capitalized name. - out.write(field.message_type.name) - else: - out.write(field.name) - - if (self.force_colon or - field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE): - # The colon is optional in this case, but our cross-language golden files - # don't include it. Here, the colon is only included if force_colon is - # set to True - out.write(':') - - def PrintField(self, field, value): - """Print a single field name/value pair.""" - self._PrintFieldName(field) - self.out.write(' ') - self.PrintFieldValue(field, value) - self.out.write(' ' if self.as_one_line else '\n') - - def _PrintShortRepeatedPrimitivesValue(self, field, value): - """"Prints short repeated primitives value.""" - # Note: this is called only when value has at least one element. - self._PrintFieldName(field) - self.out.write(' [') - for i in range(len(value) - 1): - self.PrintFieldValue(field, value[i]) - self.out.write(', ') - self.PrintFieldValue(field, value[-1]) - self.out.write(']') - self.out.write(' ' if self.as_one_line else '\n') - - def _PrintMessageFieldValue(self, value): - if self.pointy_brackets: - openb = '<' - closeb = '>' - else: - openb = '{' - closeb = '}' - - if self.as_one_line: - self.out.write('%s ' % openb) - self.PrintMessage(value) - self.out.write(closeb) - else: - self.out.write('%s\n' % openb) - self.indent += 2 - self.PrintMessage(value) - self.indent -= 2 - self.out.write(' ' * self.indent + closeb) - - def PrintFieldValue(self, field, value): - """Print a single field value (not including name). - - For repeated fields, the value should be a single element. - - Args: - field: The descriptor of the field to be printed. - value: The value of the field. - """ - out = self.out - if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: - self._PrintMessageFieldValue(value) - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: - enum_value = field.enum_type.values_by_number.get(value, None) - if enum_value is not None: - out.write(enum_value.name) - else: - out.write(str(value)) - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: - out.write('\"') - if isinstance(value, str) and not self.as_utf8: - out_value = value.encode('utf-8') - else: - out_value = value - if field.type == descriptor.FieldDescriptor.TYPE_BYTES: - # We always need to escape all binary data in TYPE_BYTES fields. - out_as_utf8 = False - else: - out_as_utf8 = self.as_utf8 - out.write(text_encoding.CEscape(out_value, out_as_utf8)) - out.write('\"') - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: - if value: - out.write('true') - else: - out.write('false') - elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: - if self.float_format is not None: - out.write('{1:{0}}'.format(self.float_format, value)) - else: - if math.isnan(value): - out.write(str(value)) - else: - out.write(str(type_checkers.ToShortestFloat(value))) - elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and - self.double_format is not None): - out.write('{1:{0}}'.format(self.double_format, value)) - else: - out.write(str(value)) - - -def Parse(text, - message, - allow_unknown_extension=False, - allow_field_number=False, - descriptor_pool=None, - allow_unknown_field=False): - """Parses a text representation of a protocol message into a message. - - NOTE: for historical reasons this function does not clear the input - message. This is different from what the binary msg.ParseFrom(...) does. - If text contains a field already set in message, the value is appended if the - field is repeated. Otherwise, an error is raised. - - Example:: - - a = MyProto() - a.repeated_field.append('test') - b = MyProto() - - # Repeated fields are combined - text_format.Parse(repr(a), b) - text_format.Parse(repr(a), b) # repeated_field contains ["test", "test"] - - # Non-repeated fields cannot be overwritten - a.singular_field = 1 - b.singular_field = 2 - text_format.Parse(repr(a), b) # ParseError - - # Binary version: - b.ParseFromString(a.SerializeToString()) # repeated_field is now "test" - - Caller is responsible for clearing the message as needed. - - Args: - text (str): Message text representation. - message (Message): A protocol buffer message to merge into. - allow_unknown_extension: if True, skip over missing extensions and keep - parsing - allow_field_number: if True, both field number and field name are allowed. - descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. - allow_unknown_field: if True, skip over unknown field and keep - parsing. Avoid to use this option if possible. It may hide some - errors (e.g. spelling error on field name) - - Returns: - Message: The same message passed as argument. - - Raises: - ParseError: On text parsing problems. - """ - return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'), - message, - allow_unknown_extension, - allow_field_number, - descriptor_pool=descriptor_pool, - allow_unknown_field=allow_unknown_field) - - -def Merge(text, - message, - allow_unknown_extension=False, - allow_field_number=False, - descriptor_pool=None, - allow_unknown_field=False): - """Parses a text representation of a protocol message into a message. - - Like Parse(), but allows repeated values for a non-repeated field, and uses - the last one. This means any non-repeated, top-level fields specified in text - replace those in the message. - - Args: - text (str): Message text representation. - message (Message): A protocol buffer message to merge into. - allow_unknown_extension: if True, skip over missing extensions and keep - parsing - allow_field_number: if True, both field number and field name are allowed. - descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. - allow_unknown_field: if True, skip over unknown field and keep - parsing. Avoid to use this option if possible. It may hide some - errors (e.g. spelling error on field name) - - Returns: - Message: The same message passed as argument. - - Raises: - ParseError: On text parsing problems. - """ - return MergeLines( - text.split(b'\n' if isinstance(text, bytes) else u'\n'), - message, - allow_unknown_extension, - allow_field_number, - descriptor_pool=descriptor_pool, - allow_unknown_field=allow_unknown_field) - - -def ParseLines(lines, - message, - allow_unknown_extension=False, - allow_field_number=False, - descriptor_pool=None, - allow_unknown_field=False): - """Parses a text representation of a protocol message into a message. - - See Parse() for caveats. - - Args: - lines: An iterable of lines of a message's text representation. - message: A protocol buffer message to merge into. - allow_unknown_extension: if True, skip over missing extensions and keep - parsing - allow_field_number: if True, both field number and field name are allowed. - descriptor_pool: A DescriptorPool used to resolve Any types. - allow_unknown_field: if True, skip over unknown field and keep - parsing. Avoid to use this option if possible. It may hide some - errors (e.g. spelling error on field name) - - Returns: - The same message passed as argument. - - Raises: - ParseError: On text parsing problems. - """ - parser = _Parser(allow_unknown_extension, - allow_field_number, - descriptor_pool=descriptor_pool, - allow_unknown_field=allow_unknown_field) - return parser.ParseLines(lines, message) - - -def MergeLines(lines, - message, - allow_unknown_extension=False, - allow_field_number=False, - descriptor_pool=None, - allow_unknown_field=False): - """Parses a text representation of a protocol message into a message. - - See Merge() for more details. - - Args: - lines: An iterable of lines of a message's text representation. - message: A protocol buffer message to merge into. - allow_unknown_extension: if True, skip over missing extensions and keep - parsing - allow_field_number: if True, both field number and field name are allowed. - descriptor_pool: A DescriptorPool used to resolve Any types. - allow_unknown_field: if True, skip over unknown field and keep - parsing. Avoid to use this option if possible. It may hide some - errors (e.g. spelling error on field name) - - Returns: - The same message passed as argument. - - Raises: - ParseError: On text parsing problems. - """ - parser = _Parser(allow_unknown_extension, - allow_field_number, - descriptor_pool=descriptor_pool, - allow_unknown_field=allow_unknown_field) - return parser.MergeLines(lines, message) - - -class _Parser(object): - """Text format parser for protocol message.""" - - def __init__(self, - allow_unknown_extension=False, - allow_field_number=False, - descriptor_pool=None, - allow_unknown_field=False): - self.allow_unknown_extension = allow_unknown_extension - self.allow_field_number = allow_field_number - self.descriptor_pool = descriptor_pool - self.allow_unknown_field = allow_unknown_field - - def ParseLines(self, lines, message): - """Parses a text representation of a protocol message into a message.""" - self._allow_multiple_scalars = False - self._ParseOrMerge(lines, message) - return message - - def MergeLines(self, lines, message): - """Merges a text representation of a protocol message into a message.""" - self._allow_multiple_scalars = True - self._ParseOrMerge(lines, message) - return message - - def _ParseOrMerge(self, lines, message): - """Converts a text representation of a protocol message into a message. - - Args: - lines: Lines of a message's text representation. - message: A protocol buffer message to merge into. - - Raises: - ParseError: On text parsing problems. - """ - # Tokenize expects native str lines. - try: - str_lines = ( - line if isinstance(line, str) else line.decode('utf-8') - for line in lines) - tokenizer = Tokenizer(str_lines) - except UnicodeDecodeError as e: - raise ParseError from e - if message: - self.root_type = message.DESCRIPTOR.full_name - while not tokenizer.AtEnd(): - self._MergeField(tokenizer, message) - - def _MergeField(self, tokenizer, message): - """Merges a single protocol message field into a message. - - Args: - tokenizer: A tokenizer to parse the field name and values. - message: A protocol message to record the data. - - Raises: - ParseError: In case of text parsing problems. - """ - message_descriptor = message.DESCRIPTOR - if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and - tokenizer.TryConsume('[')): - type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer) - tokenizer.Consume(']') - tokenizer.TryConsume(':') - self._DetectSilentMarker(tokenizer, message_descriptor.full_name, - type_url_prefix + '/' + packed_type_name) - if tokenizer.TryConsume('<'): - expanded_any_end_token = '>' - else: - tokenizer.Consume('{') - expanded_any_end_token = '}' - expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name, - self.descriptor_pool) - # Direct comparison with None is used instead of implicit bool conversion - # to avoid false positives with falsy initial values, e.g. for - # google.protobuf.ListValue. - if expanded_any_sub_message is None: - raise ParseError('Type %s not found in descriptor pool' % - packed_type_name) - while not tokenizer.TryConsume(expanded_any_end_token): - if tokenizer.AtEnd(): - raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % - (expanded_any_end_token,)) - self._MergeField(tokenizer, expanded_any_sub_message) - deterministic = False - - message.Pack(expanded_any_sub_message, - type_url_prefix=type_url_prefix, - deterministic=deterministic) - return - - if tokenizer.TryConsume('['): - name = [tokenizer.ConsumeIdentifier()] - while tokenizer.TryConsume('.'): - name.append(tokenizer.ConsumeIdentifier()) - name = '.'.join(name) - - if not message_descriptor.is_extendable: - raise tokenizer.ParseErrorPreviousToken( - 'Message type "%s" does not have extensions.' % - message_descriptor.full_name) - # pylint: disable=protected-access - field = message.Extensions._FindExtensionByName(name) - # pylint: enable=protected-access - if not field: - if self.allow_unknown_extension: - field = None - else: - raise tokenizer.ParseErrorPreviousToken( - 'Extension "%s" not registered. ' - 'Did you import the _pb2 module which defines it? ' - 'If you are trying to place the extension in the MessageSet ' - 'field of another message that is in an Any or MessageSet field, ' - 'that message\'s _pb2 module must be imported as well' % name) - elif message_descriptor != field.containing_type: - raise tokenizer.ParseErrorPreviousToken( - 'Extension "%s" does not extend message type "%s".' % - (name, message_descriptor.full_name)) - - tokenizer.Consume(']') - - else: - name = tokenizer.ConsumeIdentifierOrNumber() - if self.allow_field_number and name.isdigit(): - number = ParseInteger(name, True, True) - field = message_descriptor.fields_by_number.get(number, None) - if not field and message_descriptor.is_extendable: - field = message.Extensions._FindExtensionByNumber(number) - else: - field = message_descriptor.fields_by_name.get(name, None) - - # Group names are expected to be capitalized as they appear in the - # .proto file, which actually matches their type names, not their field - # names. - if not field: - field = message_descriptor.fields_by_name.get(name.lower(), None) - if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP: - field = None - - if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and - field.message_type.name != name): - field = None - - if not field and not self.allow_unknown_field: - raise tokenizer.ParseErrorPreviousToken( - 'Message type "%s" has no field named "%s".' % - (message_descriptor.full_name, name)) - - if field: - if not self._allow_multiple_scalars and field.containing_oneof: - # Check if there's a different field set in this oneof. - # Note that we ignore the case if the same field was set before, and we - # apply _allow_multiple_scalars to non-scalar fields as well. - which_oneof = message.WhichOneof(field.containing_oneof.name) - if which_oneof is not None and which_oneof != field.name: - raise tokenizer.ParseErrorPreviousToken( - 'Field "%s" is specified along with field "%s", another member ' - 'of oneof "%s" for message type "%s".' % - (field.name, which_oneof, field.containing_oneof.name, - message_descriptor.full_name)) - - if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: - tokenizer.TryConsume(':') - self._DetectSilentMarker(tokenizer, message_descriptor.full_name, - field.full_name) - merger = self._MergeMessageField - else: - tokenizer.Consume(':') - self._DetectSilentMarker(tokenizer, message_descriptor.full_name, - field.full_name) - merger = self._MergeScalarField - - if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and - tokenizer.TryConsume('[')): - # Short repeated format, e.g. "foo: [1, 2, 3]" - if not tokenizer.TryConsume(']'): - while True: - merger(tokenizer, message, field) - if tokenizer.TryConsume(']'): - break - tokenizer.Consume(',') - - else: - merger(tokenizer, message, field) - - else: # Proto field is unknown. - assert (self.allow_unknown_extension or self.allow_unknown_field) - self._SkipFieldContents(tokenizer, name, message_descriptor.full_name) - - # For historical reasons, fields may optionally be separated by commas or - # semicolons. - if not tokenizer.TryConsume(','): - tokenizer.TryConsume(';') - - def _LogSilentMarker(self, immediate_message_type, field_name): - pass - - def _DetectSilentMarker(self, tokenizer, immediate_message_type, field_name): - if tokenizer.contains_silent_marker_before_current_token: - self._LogSilentMarker(immediate_message_type, field_name) - - def _ConsumeAnyTypeUrl(self, tokenizer): - """Consumes a google.protobuf.Any type URL and returns the type name.""" - # Consume "type.googleapis.com/". - prefix = [tokenizer.ConsumeIdentifier()] - tokenizer.Consume('.') - prefix.append(tokenizer.ConsumeIdentifier()) - tokenizer.Consume('.') - prefix.append(tokenizer.ConsumeIdentifier()) - tokenizer.Consume('/') - # Consume the fully-qualified type name. - name = [tokenizer.ConsumeIdentifier()] - while tokenizer.TryConsume('.'): - name.append(tokenizer.ConsumeIdentifier()) - return '.'.join(prefix), '.'.join(name) - - def _MergeMessageField(self, tokenizer, message, field): - """Merges a single scalar field into a message. - - Args: - tokenizer: A tokenizer to parse the field value. - message: The message of which field is a member. - field: The descriptor of the field to be merged. - - Raises: - ParseError: In case of text parsing problems. - """ - is_map_entry = _IsMapEntry(field) - - if tokenizer.TryConsume('<'): - end_token = '>' - else: - tokenizer.Consume('{') - end_token = '}' - - if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: - if field.is_extension: - sub_message = message.Extensions[field].add() - elif is_map_entry: - sub_message = getattr(message, field.name).GetEntryClass()() - else: - sub_message = getattr(message, field.name).add() - else: - if field.is_extension: - if (not self._allow_multiple_scalars and - message.HasExtension(field)): - raise tokenizer.ParseErrorPreviousToken( - 'Message type "%s" should not have multiple "%s" extensions.' % - (message.DESCRIPTOR.full_name, field.full_name)) - sub_message = message.Extensions[field] - else: - # Also apply _allow_multiple_scalars to message field. - # TODO(jieluo): Change to _allow_singular_overwrites. - if (not self._allow_multiple_scalars and - message.HasField(field.name)): - raise tokenizer.ParseErrorPreviousToken( - 'Message type "%s" should not have multiple "%s" fields.' % - (message.DESCRIPTOR.full_name, field.name)) - sub_message = getattr(message, field.name) - sub_message.SetInParent() - - while not tokenizer.TryConsume(end_token): - if tokenizer.AtEnd(): - raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,)) - self._MergeField(tokenizer, sub_message) - - if is_map_entry: - value_cpptype = field.message_type.fields_by_name['value'].cpp_type - if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: - value = getattr(message, field.name)[sub_message.key] - value.CopyFrom(sub_message.value) - else: - getattr(message, field.name)[sub_message.key] = sub_message.value - - def _MergeScalarField(self, tokenizer, message, field): - """Merges a single scalar field into a message. - - Args: - tokenizer: A tokenizer to parse the field value. - message: A protocol message to record the data. - field: The descriptor of the field to be merged. - - Raises: - ParseError: In case of text parsing problems. - RuntimeError: On runtime errors. - """ - _ = self.allow_unknown_extension - value = None - - if field.type in (descriptor.FieldDescriptor.TYPE_INT32, - descriptor.FieldDescriptor.TYPE_SINT32, - descriptor.FieldDescriptor.TYPE_SFIXED32): - value = _ConsumeInt32(tokenizer) - elif field.type in (descriptor.FieldDescriptor.TYPE_INT64, - descriptor.FieldDescriptor.TYPE_SINT64, - descriptor.FieldDescriptor.TYPE_SFIXED64): - value = _ConsumeInt64(tokenizer) - elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32, - descriptor.FieldDescriptor.TYPE_FIXED32): - value = _ConsumeUint32(tokenizer) - elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64, - descriptor.FieldDescriptor.TYPE_FIXED64): - value = _ConsumeUint64(tokenizer) - elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT, - descriptor.FieldDescriptor.TYPE_DOUBLE): - value = tokenizer.ConsumeFloat() - elif field.type == descriptor.FieldDescriptor.TYPE_BOOL: - value = tokenizer.ConsumeBool() - elif field.type == descriptor.FieldDescriptor.TYPE_STRING: - value = tokenizer.ConsumeString() - elif field.type == descriptor.FieldDescriptor.TYPE_BYTES: - value = tokenizer.ConsumeByteString() - elif field.type == descriptor.FieldDescriptor.TYPE_ENUM: - value = tokenizer.ConsumeEnum(field) - else: - raise RuntimeError('Unknown field type %d' % field.type) - - if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: - if field.is_extension: - message.Extensions[field].append(value) - else: - getattr(message, field.name).append(value) - else: - if field.is_extension: - if (not self._allow_multiple_scalars and - field.has_presence and - message.HasExtension(field)): - raise tokenizer.ParseErrorPreviousToken( - 'Message type "%s" should not have multiple "%s" extensions.' % - (message.DESCRIPTOR.full_name, field.full_name)) - else: - message.Extensions[field] = value - else: - duplicate_error = False - if not self._allow_multiple_scalars: - if field.has_presence: - duplicate_error = message.HasField(field.name) - else: - # For field that doesn't represent presence, try best effort to - # check multiple scalars by compare to default values. - duplicate_error = bool(getattr(message, field.name)) - - if duplicate_error: - raise tokenizer.ParseErrorPreviousToken( - 'Message type "%s" should not have multiple "%s" fields.' % - (message.DESCRIPTOR.full_name, field.name)) - else: - setattr(message, field.name, value) - - def _SkipFieldContents(self, tokenizer, field_name, immediate_message_type): - """Skips over contents (value or message) of a field. - - Args: - tokenizer: A tokenizer to parse the field name and values. - field_name: The field name currently being parsed. - immediate_message_type: The type of the message immediately containing - the silent marker. - """ - # Try to guess the type of this field. - # If this field is not a message, there should be a ":" between the - # field name and the field value and also the field value should not - # start with "{" or "<" which indicates the beginning of a message body. - # If there is no ":" or there is a "{" or "<" after ":", this field has - # to be a message or the input is ill-formed. - if tokenizer.TryConsume( - ':') and not tokenizer.LookingAt('{') and not tokenizer.LookingAt('<'): - self._DetectSilentMarker(tokenizer, immediate_message_type, field_name) - if tokenizer.LookingAt('['): - self._SkipRepeatedFieldValue(tokenizer) - else: - self._SkipFieldValue(tokenizer) - else: - self._DetectSilentMarker(tokenizer, immediate_message_type, field_name) - self._SkipFieldMessage(tokenizer, immediate_message_type) - - def _SkipField(self, tokenizer, immediate_message_type): - """Skips over a complete field (name and value/message). - - Args: - tokenizer: A tokenizer to parse the field name and values. - immediate_message_type: The type of the message immediately containing - the silent marker. - """ - field_name = '' - if tokenizer.TryConsume('['): - # Consume extension or google.protobuf.Any type URL - field_name += '[' + tokenizer.ConsumeIdentifier() - num_identifiers = 1 - while tokenizer.TryConsume('.'): - field_name += '.' + tokenizer.ConsumeIdentifier() - num_identifiers += 1 - # This is possibly a type URL for an Any message. - if num_identifiers == 3 and tokenizer.TryConsume('/'): - field_name += '/' + tokenizer.ConsumeIdentifier() - while tokenizer.TryConsume('.'): - field_name += '.' + tokenizer.ConsumeIdentifier() - tokenizer.Consume(']') - field_name += ']' - else: - field_name += tokenizer.ConsumeIdentifierOrNumber() - - self._SkipFieldContents(tokenizer, field_name, immediate_message_type) - - # For historical reasons, fields may optionally be separated by commas or - # semicolons. - if not tokenizer.TryConsume(','): - tokenizer.TryConsume(';') - - def _SkipFieldMessage(self, tokenizer, immediate_message_type): - """Skips over a field message. - - Args: - tokenizer: A tokenizer to parse the field name and values. - immediate_message_type: The type of the message immediately containing - the silent marker - """ - if tokenizer.TryConsume('<'): - delimiter = '>' - else: - tokenizer.Consume('{') - delimiter = '}' - - while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'): - self._SkipField(tokenizer, immediate_message_type) - - tokenizer.Consume(delimiter) - - def _SkipFieldValue(self, tokenizer): - """Skips over a field value. - - Args: - tokenizer: A tokenizer to parse the field name and values. - - Raises: - ParseError: In case an invalid field value is found. - """ - if (not tokenizer.TryConsumeByteString()and - not tokenizer.TryConsumeIdentifier() and - not _TryConsumeInt64(tokenizer) and - not _TryConsumeUint64(tokenizer) and - not tokenizer.TryConsumeFloat()): - raise ParseError('Invalid field value: ' + tokenizer.token) - - def _SkipRepeatedFieldValue(self, tokenizer): - """Skips over a repeated field value. - - Args: - tokenizer: A tokenizer to parse the field value. - """ - tokenizer.Consume('[') - if not tokenizer.LookingAt(']'): - self._SkipFieldValue(tokenizer) - while tokenizer.TryConsume(','): - self._SkipFieldValue(tokenizer) - tokenizer.Consume(']') - - -class Tokenizer(object): - """Protocol buffer text representation tokenizer. - - This class handles the lower level string parsing by splitting it into - meaningful tokens. - - It was directly ported from the Java protocol buffer API. - """ - - _WHITESPACE = re.compile(r'\s+') - _COMMENT = re.compile(r'(\s*#.*$)', re.MULTILINE) - _WHITESPACE_OR_COMMENT = re.compile(r'(\s|(#.*$))+', re.MULTILINE) - _TOKEN = re.compile('|'.join([ - r'[a-zA-Z_][0-9a-zA-Z_+-]*', # an identifier - r'([0-9+-]|(\.[0-9]))[0-9a-zA-Z_.+-]*', # a number - ] + [ # quoted str for each quote mark - # Avoid backtracking! https://stackoverflow.com/a/844267 - r'{qt}[^{qt}\n\\]*((\\.)+[^{qt}\n\\]*)*({qt}|\\?$)'.format(qt=mark) - for mark in _QUOTES - ])) - - _IDENTIFIER = re.compile(r'[^\d\W]\w*') - _IDENTIFIER_OR_NUMBER = re.compile(r'\w+') - - def __init__(self, lines, skip_comments=True): - self._position = 0 - self._line = -1 - self._column = 0 - self._token_start = None - self.token = '' - self._lines = iter(lines) - self._current_line = '' - self._previous_line = 0 - self._previous_column = 0 - self._more_lines = True - self._skip_comments = skip_comments - self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT - or self._WHITESPACE) - self.contains_silent_marker_before_current_token = False - - self._SkipWhitespace() - self.NextToken() - - def LookingAt(self, token): - return self.token == token - - def AtEnd(self): - """Checks the end of the text was reached. - - Returns: - True iff the end was reached. - """ - return not self.token - - def _PopLine(self): - while len(self._current_line) <= self._column: - try: - self._current_line = next(self._lines) - except StopIteration: - self._current_line = '' - self._more_lines = False - return - else: - self._line += 1 - self._column = 0 - - def _SkipWhitespace(self): - while True: - self._PopLine() - match = self._whitespace_pattern.match(self._current_line, self._column) - if not match: - break - self.contains_silent_marker_before_current_token = match.group(0) == ( - ' ' + _DEBUG_STRING_SILENT_MARKER) - length = len(match.group(0)) - self._column += length - - def TryConsume(self, token): - """Tries to consume a given piece of text. - - Args: - token: Text to consume. - - Returns: - True iff the text was consumed. - """ - if self.token == token: - self.NextToken() - return True - return False - - def Consume(self, token): - """Consumes a piece of text. - - Args: - token: Text to consume. - - Raises: - ParseError: If the text couldn't be consumed. - """ - if not self.TryConsume(token): - raise self.ParseError('Expected "%s".' % token) - - def ConsumeComment(self): - result = self.token - if not self._COMMENT.match(result): - raise self.ParseError('Expected comment.') - self.NextToken() - return result - - def ConsumeCommentOrTrailingComment(self): - """Consumes a comment, returns a 2-tuple (trailing bool, comment str).""" - - # Tokenizer initializes _previous_line and _previous_column to 0. As the - # tokenizer starts, it looks like there is a previous token on the line. - just_started = self._line == 0 and self._column == 0 - - before_parsing = self._previous_line - comment = self.ConsumeComment() - - # A trailing comment is a comment on the same line than the previous token. - trailing = (self._previous_line == before_parsing - and not just_started) - - return trailing, comment - - def TryConsumeIdentifier(self): - try: - self.ConsumeIdentifier() - return True - except ParseError: - return False - - def ConsumeIdentifier(self): - """Consumes protocol message field identifier. - - Returns: - Identifier string. - - Raises: - ParseError: If an identifier couldn't be consumed. - """ - result = self.token - if not self._IDENTIFIER.match(result): - raise self.ParseError('Expected identifier.') - self.NextToken() - return result - - def TryConsumeIdentifierOrNumber(self): - try: - self.ConsumeIdentifierOrNumber() - return True - except ParseError: - return False - - def ConsumeIdentifierOrNumber(self): - """Consumes protocol message field identifier. - - Returns: - Identifier string. - - Raises: - ParseError: If an identifier couldn't be consumed. - """ - result = self.token - if not self._IDENTIFIER_OR_NUMBER.match(result): - raise self.ParseError('Expected identifier or number, got %s.' % result) - self.NextToken() - return result - - def TryConsumeInteger(self): - try: - self.ConsumeInteger() - return True - except ParseError: - return False - - def ConsumeInteger(self): - """Consumes an integer number. - - Returns: - The integer parsed. - - Raises: - ParseError: If an integer couldn't be consumed. - """ - try: - result = _ParseAbstractInteger(self.token) - except ValueError as e: - raise self.ParseError(str(e)) - self.NextToken() - return result - - def TryConsumeFloat(self): - try: - self.ConsumeFloat() - return True - except ParseError: - return False - - def ConsumeFloat(self): - """Consumes an floating point number. - - Returns: - The number parsed. - - Raises: - ParseError: If a floating point number couldn't be consumed. - """ - try: - result = ParseFloat(self.token) - except ValueError as e: - raise self.ParseError(str(e)) - self.NextToken() - return result - - def ConsumeBool(self): - """Consumes a boolean value. - - Returns: - The bool parsed. - - Raises: - ParseError: If a boolean value couldn't be consumed. - """ - try: - result = ParseBool(self.token) - except ValueError as e: - raise self.ParseError(str(e)) - self.NextToken() - return result - - def TryConsumeByteString(self): - try: - self.ConsumeByteString() - return True - except ParseError: - return False - - def ConsumeString(self): - """Consumes a string value. - - Returns: - The string parsed. - - Raises: - ParseError: If a string value couldn't be consumed. - """ - the_bytes = self.ConsumeByteString() - try: - return str(the_bytes, 'utf-8') - except UnicodeDecodeError as e: - raise self._StringParseError(e) - - def ConsumeByteString(self): - """Consumes a byte array value. - - Returns: - The array parsed (as a string). - - Raises: - ParseError: If a byte array value couldn't be consumed. - """ - the_list = [self._ConsumeSingleByteString()] - while self.token and self.token[0] in _QUOTES: - the_list.append(self._ConsumeSingleByteString()) - return b''.join(the_list) - - def _ConsumeSingleByteString(self): - """Consume one token of a string literal. - - String literals (whether bytes or text) can come in multiple adjacent - tokens which are automatically concatenated, like in C or Python. This - method only consumes one token. - - Returns: - The token parsed. - Raises: - ParseError: When the wrong format data is found. - """ - text = self.token - if len(text) < 1 or text[0] not in _QUOTES: - raise self.ParseError('Expected string but found: %r' % (text,)) - - if len(text) < 2 or text[-1] != text[0]: - raise self.ParseError('String missing ending quote: %r' % (text,)) - - try: - result = text_encoding.CUnescape(text[1:-1]) - except ValueError as e: - raise self.ParseError(str(e)) - self.NextToken() - return result - - def ConsumeEnum(self, field): - try: - result = ParseEnum(field, self.token) - except ValueError as e: - raise self.ParseError(str(e)) - self.NextToken() - return result - - def ParseErrorPreviousToken(self, message): - """Creates and *returns* a ParseError for the previously read token. - - Args: - message: A message to set for the exception. - - Returns: - A ParseError instance. - """ - return ParseError(message, self._previous_line + 1, - self._previous_column + 1) - - def ParseError(self, message): - """Creates and *returns* a ParseError for the current token.""" - return ParseError('\'' + self._current_line + '\': ' + message, - self._line + 1, self._column + 1) - - def _StringParseError(self, e): - return self.ParseError('Couldn\'t parse string: ' + str(e)) - - def NextToken(self): - """Reads the next meaningful token.""" - self._previous_line = self._line - self._previous_column = self._column - self.contains_silent_marker_before_current_token = False - - self._column += len(self.token) - self._SkipWhitespace() - - if not self._more_lines: - self.token = '' - return - - match = self._TOKEN.match(self._current_line, self._column) - if not match and not self._skip_comments: - match = self._COMMENT.match(self._current_line, self._column) - if match: - token = match.group(0) - self.token = token - else: - self.token = self._current_line[self._column] - -# Aliased so it can still be accessed by current visibility violators. -# TODO(dbarnett): Migrate violators to textformat_tokenizer. -_Tokenizer = Tokenizer # pylint: disable=invalid-name - - -def _ConsumeInt32(tokenizer): - """Consumes a signed 32bit integer number from tokenizer. - - Args: - tokenizer: A tokenizer used to parse the number. - - Returns: - The integer parsed. - - Raises: - ParseError: If a signed 32bit integer couldn't be consumed. - """ - return _ConsumeInteger(tokenizer, is_signed=True, is_long=False) - - -def _ConsumeUint32(tokenizer): - """Consumes an unsigned 32bit integer number from tokenizer. - - Args: - tokenizer: A tokenizer used to parse the number. - - Returns: - The integer parsed. - - Raises: - ParseError: If an unsigned 32bit integer couldn't be consumed. - """ - return _ConsumeInteger(tokenizer, is_signed=False, is_long=False) - - -def _TryConsumeInt64(tokenizer): - try: - _ConsumeInt64(tokenizer) - return True - except ParseError: - return False - - -def _ConsumeInt64(tokenizer): - """Consumes a signed 32bit integer number from tokenizer. - - Args: - tokenizer: A tokenizer used to parse the number. - - Returns: - The integer parsed. - - Raises: - ParseError: If a signed 32bit integer couldn't be consumed. - """ - return _ConsumeInteger(tokenizer, is_signed=True, is_long=True) - - -def _TryConsumeUint64(tokenizer): - try: - _ConsumeUint64(tokenizer) - return True - except ParseError: - return False - - -def _ConsumeUint64(tokenizer): - """Consumes an unsigned 64bit integer number from tokenizer. - - Args: - tokenizer: A tokenizer used to parse the number. - - Returns: - The integer parsed. - - Raises: - ParseError: If an unsigned 64bit integer couldn't be consumed. - """ - return _ConsumeInteger(tokenizer, is_signed=False, is_long=True) - - -def _ConsumeInteger(tokenizer, is_signed=False, is_long=False): - """Consumes an integer number from tokenizer. - - Args: - tokenizer: A tokenizer used to parse the number. - is_signed: True if a signed integer must be parsed. - is_long: True if a long integer must be parsed. - - Returns: - The integer parsed. - - Raises: - ParseError: If an integer with given characteristics couldn't be consumed. - """ - try: - result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long) - except ValueError as e: - raise tokenizer.ParseError(str(e)) - tokenizer.NextToken() - return result - - -def ParseInteger(text, is_signed=False, is_long=False): - """Parses an integer. - - Args: - text: The text to parse. - is_signed: True if a signed integer must be parsed. - is_long: True if a long integer must be parsed. - - Returns: - The integer value. - - Raises: - ValueError: Thrown Iff the text is not a valid integer. - """ - # Do the actual parsing. Exception handling is propagated to caller. - result = _ParseAbstractInteger(text) - - # Check if the integer is sane. Exceptions handled by callers. - checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)] - checker.CheckValue(result) - return result - - -def _ParseAbstractInteger(text): - """Parses an integer without checking size/signedness. - - Args: - text: The text to parse. - - Returns: - The integer value. - - Raises: - ValueError: Thrown Iff the text is not a valid integer. - """ - # Do the actual parsing. Exception handling is propagated to caller. - orig_text = text - c_octal_match = re.match(r'(-?)0(\d+)$', text) - if c_octal_match: - # Python 3 no longer supports 0755 octal syntax without the 'o', so - # we always use the '0o' prefix for multi-digit numbers starting with 0. - text = c_octal_match.group(1) + '0o' + c_octal_match.group(2) - try: - return int(text, 0) - except ValueError: - raise ValueError('Couldn\'t parse integer: %s' % orig_text) - - -def ParseFloat(text): - """Parse a floating point number. - - Args: - text: Text to parse. - - Returns: - The number parsed. - - Raises: - ValueError: If a floating point number couldn't be parsed. - """ - try: - # Assume Python compatible syntax. - return float(text) - except ValueError: - # Check alternative spellings. - if _FLOAT_INFINITY.match(text): - if text[0] == '-': - return float('-inf') - else: - return float('inf') - elif _FLOAT_NAN.match(text): - return float('nan') - else: - # assume '1.0f' format - try: - return float(text.rstrip('f')) - except ValueError: - raise ValueError('Couldn\'t parse float: %s' % text) - - -def ParseBool(text): - """Parse a boolean value. - - Args: - text: Text to parse. - - Returns: - Boolean values parsed - - Raises: - ValueError: If text is not a valid boolean. - """ - if text in ('true', 't', '1', 'True'): - return True - elif text in ('false', 'f', '0', 'False'): - return False - else: - raise ValueError('Expected "true" or "false".') - - -def ParseEnum(field, value): - """Parse an enum value. - - The value can be specified by a number (the enum value), or by - a string literal (the enum name). - - Args: - field: Enum field descriptor. - value: String value. - - Returns: - Enum value number. - - Raises: - ValueError: If the enum value could not be parsed. - """ - enum_descriptor = field.enum_type - try: - number = int(value, 0) - except ValueError: - # Identifier. - enum_value = enum_descriptor.values_by_name.get(value, None) - if enum_value is None: - raise ValueError('Enum type "%s" has no value named %s.' % - (enum_descriptor.full_name, value)) - else: - if not field.enum_type.is_closed: - return number - enum_value = enum_descriptor.values_by_number.get(number, None) - if enum_value is None: - raise ValueError('Enum type "%s" has no value with number %d.' % - (enum_descriptor.full_name, number)) - return enum_value.number diff --git a/script.module.protobuf/resources/lib/google/protobuf/timestamp_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/timestamp_pb2.py deleted file mode 100644 index b10f2f204..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/timestamp_pb2.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/timestamp.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgoogle/protobuf/timestamp.proto\x12\x0fgoogle.protobuf\";\n\tTimestamp\x12\x18\n\x07seconds\x18\x01 \x01(\x03R\x07seconds\x12\x14\n\x05nanos\x18\x02 \x01(\x05R\x05nanosB\x85\x01\n\x13\x63om.google.protobufB\x0eTimestampProtoP\x01Z2google.golang.org/protobuf/types/known/timestamppb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016TimestampProtoP\001Z2google.golang.org/protobuf/types/known/timestamppb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_TIMESTAMP']._serialized_start=52 - _globals['_TIMESTAMP']._serialized_end=111 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/type_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/type_pb2.py deleted file mode 100644 index ca8a4e20e..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/type_pb2.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/type.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1agoogle/protobuf/type.proto\x12\x0fgoogle.protobuf\x1a\x19google/protobuf/any.proto\x1a$google/protobuf/source_context.proto\"\xa7\x02\n\x04Type\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12.\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x16.google.protobuf.FieldR\x06\x66ields\x12\x16\n\x06oneofs\x18\x03 \x03(\tR\x06oneofs\x12\x31\n\x07options\x18\x04 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x45\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContextR\rsourceContext\x12/\n\x06syntax\x18\x06 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\x12\x18\n\x07\x65\x64ition\x18\x07 \x01(\tR\x07\x65\x64ition\"\xb4\x06\n\x05\x46ield\x12/\n\x04kind\x18\x01 \x01(\x0e\x32\x1b.google.protobuf.Field.KindR\x04kind\x12\x44\n\x0b\x63\x61rdinality\x18\x02 \x01(\x0e\x32\".google.protobuf.Field.CardinalityR\x0b\x63\x61rdinality\x12\x16\n\x06number\x18\x03 \x01(\x05R\x06number\x12\x12\n\x04name\x18\x04 \x01(\tR\x04name\x12\x19\n\x08type_url\x18\x06 \x01(\tR\x07typeUrl\x12\x1f\n\x0boneof_index\x18\x07 \x01(\x05R\noneofIndex\x12\x16\n\x06packed\x18\x08 \x01(\x08R\x06packed\x12\x31\n\x07options\x18\t \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x1b\n\tjson_name\x18\n \x01(\tR\x08jsonName\x12#\n\rdefault_value\x18\x0b \x01(\tR\x0c\x64\x65\x66\x61ultValue\"\xc8\x02\n\x04Kind\x12\x10\n\x0cTYPE_UNKNOWN\x10\x00\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"t\n\x0b\x43\x61rdinality\x12\x17\n\x13\x43\x41RDINALITY_UNKNOWN\x10\x00\x12\x18\n\x14\x43\x41RDINALITY_OPTIONAL\x10\x01\x12\x18\n\x14\x43\x41RDINALITY_REQUIRED\x10\x02\x12\x18\n\x14\x43\x41RDINALITY_REPEATED\x10\x03\"\x99\x02\n\x04\x45num\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x38\n\tenumvalue\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.EnumValueR\tenumvalue\x12\x31\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x45\n\x0esource_context\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.SourceContextR\rsourceContext\x12/\n\x06syntax\x18\x05 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\x12\x18\n\x07\x65\x64ition\x18\x06 \x01(\tR\x07\x65\x64ition\"j\n\tEnumValue\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x02 \x01(\x05R\x06number\x12\x31\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\"H\n\x06Option\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.AnyR\x05value*C\n\x06Syntax\x12\x11\n\rSYNTAX_PROTO2\x10\x00\x12\x11\n\rSYNTAX_PROTO3\x10\x01\x12\x13\n\x0fSYNTAX_EDITIONS\x10\x02\x42{\n\x13\x63om.google.protobufB\tTypeProtoP\x01Z-google.golang.org/protobuf/types/known/typepb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\tTypeProtoP\001Z-google.golang.org/protobuf/types/known/typepb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_SYNTAX']._serialized_start=1699 - _globals['_SYNTAX']._serialized_end=1766 - _globals['_TYPE']._serialized_start=113 - _globals['_TYPE']._serialized_end=408 - _globals['_FIELD']._serialized_start=411 - _globals['_FIELD']._serialized_end=1231 - _globals['_FIELD_KIND']._serialized_start=785 - _globals['_FIELD_KIND']._serialized_end=1113 - _globals['_FIELD_CARDINALITY']._serialized_start=1115 - _globals['_FIELD_CARDINALITY']._serialized_end=1231 - _globals['_ENUM']._serialized_start=1234 - _globals['_ENUM']._serialized_end=1515 - _globals['_ENUMVALUE']._serialized_start=1517 - _globals['_ENUMVALUE']._serialized_end=1623 - _globals['_OPTION']._serialized_start=1625 - _globals['_OPTION']._serialized_end=1697 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.protobuf/resources/lib/google/protobuf/unknown_fields.py b/script.module.protobuf/resources/lib/google/protobuf/unknown_fields.py deleted file mode 100644 index 3bd828619..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/unknown_fields.py +++ /dev/null @@ -1,120 +0,0 @@ -# Protocol Buffers - Google's data interchange format -# Copyright 2008 Google Inc. All rights reserved. -# https://developers.google.com/protocol-buffers/ -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Contains Unknown Fields APIs. - -Simple usage example: - unknown_field_set = UnknownFieldSet(message) - for unknown_field in unknown_field_set: - wire_type = unknown_field.wire_type - field_number = unknown_field.field_number - data = unknown_field.data -""" - - -from google.protobuf.internal import api_implementation - -if api_implementation._c_module is not None: # pylint: disable=protected-access - UnknownFieldSet = api_implementation._c_module.UnknownFieldSet # pylint: disable=protected-access -else: - from google.protobuf.internal import decoder # pylint: disable=g-import-not-at-top - from google.protobuf.internal import wire_format # pylint: disable=g-import-not-at-top - - class UnknownField: - """A parsed unknown field.""" - - # Disallows assignment to other attributes. - __slots__ = ['_field_number', '_wire_type', '_data'] - - def __init__(self, field_number, wire_type, data): - self._field_number = field_number - self._wire_type = wire_type - self._data = data - return - - @property - def field_number(self): - return self._field_number - - @property - def wire_type(self): - return self._wire_type - - @property - def data(self): - return self._data - - class UnknownFieldSet: - """UnknownField container.""" - - # Disallows assignment to other attributes. - __slots__ = ['_values'] - - def __init__(self, msg): - - def InternalAdd(field_number, wire_type, data): - unknown_field = UnknownField(field_number, wire_type, data) - self._values.append(unknown_field) - - self._values = [] - msg_des = msg.DESCRIPTOR - # pylint: disable=protected-access - unknown_fields = msg._unknown_fields - if (msg_des.has_options and - msg_des.GetOptions().message_set_wire_format): - local_decoder = decoder.UnknownMessageSetItemDecoder() - for _, buffer in unknown_fields: - (field_number, data) = local_decoder(memoryview(buffer)) - InternalAdd(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED, data) - else: - for tag_bytes, buffer in unknown_fields: - # pylint: disable=protected-access - (tag, _) = decoder._DecodeVarint(tag_bytes, 0) - field_number, wire_type = wire_format.UnpackTag(tag) - if field_number == 0: - raise RuntimeError('Field number 0 is illegal.') - (data, _) = decoder._DecodeUnknownField( - memoryview(buffer), 0, wire_type) - InternalAdd(field_number, wire_type, data) - - def __getitem__(self, index): - size = len(self._values) - if index < 0: - index += size - if index < 0 or index >= size: - raise IndexError('index %d out of range'.index) - - return self._values[index] - - def __len__(self): - return len(self._values) - - def __iter__(self): - return iter(self._values) diff --git a/script.module.protobuf/resources/lib/google/protobuf/util/__init__.py b/script.module.protobuf/resources/lib/google/protobuf/util/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.module.protobuf/resources/lib/google/protobuf/wrappers_pb2.py b/script.module.protobuf/resources/lib/google/protobuf/wrappers_pb2.py deleted file mode 100644 index 035bddcb5..000000000 --- a/script.module.protobuf/resources/lib/google/protobuf/wrappers_pb2.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google/protobuf/wrappers.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/wrappers.proto\x12\x0fgoogle.protobuf\"#\n\x0b\x44oubleValue\x12\x14\n\x05value\x18\x01 \x01(\x01R\x05value\"\"\n\nFloatValue\x12\x14\n\x05value\x18\x01 \x01(\x02R\x05value\"\"\n\nInt64Value\x12\x14\n\x05value\x18\x01 \x01(\x03R\x05value\"#\n\x0bUInt64Value\x12\x14\n\x05value\x18\x01 \x01(\x04R\x05value\"\"\n\nInt32Value\x12\x14\n\x05value\x18\x01 \x01(\x05R\x05value\"#\n\x0bUInt32Value\x12\x14\n\x05value\x18\x01 \x01(\rR\x05value\"!\n\tBoolValue\x12\x14\n\x05value\x18\x01 \x01(\x08R\x05value\"#\n\x0bStringValue\x12\x14\n\x05value\x18\x01 \x01(\tR\x05value\"\"\n\nBytesValue\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05valueB\x83\x01\n\x13\x63om.google.protobufB\rWrappersProtoP\x01Z1google.golang.org/protobuf/types/known/wrapperspb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rWrappersProtoP\001Z1google.golang.org/protobuf/types/known/wrapperspb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' - _globals['_DOUBLEVALUE']._serialized_start=51 - _globals['_DOUBLEVALUE']._serialized_end=86 - _globals['_FLOATVALUE']._serialized_start=88 - _globals['_FLOATVALUE']._serialized_end=122 - _globals['_INT64VALUE']._serialized_start=124 - _globals['_INT64VALUE']._serialized_end=158 - _globals['_UINT64VALUE']._serialized_start=160 - _globals['_UINT64VALUE']._serialized_end=195 - _globals['_INT32VALUE']._serialized_start=197 - _globals['_INT32VALUE']._serialized_end=231 - _globals['_UINT32VALUE']._serialized_start=233 - _globals['_UINT32VALUE']._serialized_end=268 - _globals['_BOOLVALUE']._serialized_start=270 - _globals['_BOOLVALUE']._serialized_end=303 - _globals['_STRINGVALUE']._serialized_start=305 - _globals['_STRINGVALUE']._serialized_end=340 - _globals['_BYTESVALUE']._serialized_start=342 - _globals['_BYTESVALUE']._serialized_end=376 -# @@protoc_insertion_point(module_scope) diff --git a/script.module.python.twitch/LICENSES/GPL-3.0-only b/script.module.python.twitch/LICENSES/GPL-3.0-only deleted file mode 100644 index 9cecc1d46..000000000 --- a/script.module.python.twitch/LICENSES/GPL-3.0-only +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -them 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 prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey 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; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - 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. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -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. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/script.module.python.twitch/addon.xml b/script.module.python.twitch/addon.xml deleted file mode 100644 index c9173fc3e..000000000 --- a/script.module.python.twitch/addon.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - all - -[fix] Following Channels - - - resources/media/icon.png - - all - GPL-3.0-only - https://github.com/anxdpanic/script.module.python.twitch - https://twitchaddon.panicked.xyz/forum - true - Modul til interaktion med Twitch.tv API - Modul für die Interaktion mit der Twitch.tv-API - Module for interaction with the Twitch.tv API - Módulo para interactuar con la API de Twitch.tv - Moduuli Twitch.tv-rajapinnan käyttöön - Module pour interagir avec l'API de Twitch.tv - Modul za interakciju s Twitch.tv API - Modul a Twitch.tv API -val való interakcióhoz - Modulo per l'interazione con l'API di Twitch.tv - Twitch.tv API와의 상호작용을 위한 모듈 - Moduł do interakcji z API Twitch.tv - Модуль для взаимодействия с Twitch.tv API - 与 Twitch.tv API 交互的模块 - python-twitch til Kodi er et modul til interaktion med Twitch.tv API baseret på python-twitch af ingwinlu. - python-twitch für Kodi ist ein Modul für die Interaktion mit der Twitch.tv-API, basierend auf python-twitch von ingwinlu. - python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu. - python-twitch para Kodi es un módulo para interactuar con la API de Twitch.tv basado en python-twitch de ingwinlu. - 'Python-twitch for Kodi' on Twitch.tv-palvelun rajapinnan hyödyntämisen mahdollistava moduuli, joka perustuu kehittäjän ingwinlu luomaan python-twitch-moduuliin. - python-twitch pour Kodi est un module pour interagir avec l'API Twitch.tv basé sur python-twitch de ingwinlu. - python-twitch za Kodi je modul za interakciju s Twitch.tv API-jem temeljenom na python-twitch by ingwinlu. - A python-twitch a Kodi számára a Twitch.tv API-val való interakció modulja, amely az ingwinlu python-twitch-jén alapul. - python-twitch per Kodi è un modulo per l'interazione con l'API di Twitch.tv basato su python-twitch di ingwinlu. - Kodi용 python-twitch는 ingwinlu의 python-twitch를 기반으로 하는 Twitch.tv API와 상호 작용하기 위한 모듈입니다. - python-twitch dla Kodi to moduł do interakcji z API Twitch.tv oparty na python-twitch autorstwa ingwinlu. - python-twitch это модуль для взаимодействия с Twitch.tv API. Основан на модуле python-twitch от ingwinlu. - Kodi 的 python-twitch 是从 ingwinlu 开发版本移植的与 twitch.tv API 交互的模块。 - - diff --git a/script.module.python.twitch/changelog.txt b/script.module.python.twitch/changelog.txt deleted file mode 100644 index 6643b4e59..000000000 --- a/script.module.python.twitch/changelog.txt +++ /dev/null @@ -1,108 +0,0 @@ -3.0.0 -[rem] removed support for Kodi versions pre-Nexus v20 -[rem] removed python 2 support -[rem] removed dependency on six -[lang] updated translations from Weblate - -2.0.21 -[upd] fixups and additions to Helix API -[rem] remove v5 API -[lang] updated translations from Weblate - -2.0.19 -[fix] fix v5.users.follow_channel request - apo86 -[fix] fix v5.users.unfollow_channel request - apo86 -[lang] updated translations from Weblate - -2.0.18 -[fix] fix v5.streams.by_id request - apo86 - -2.0.17 -[fix] playback of clips - -2.0.16 -[fix] playback, change access token to use gql endpoints -[chg] change followed, following, and unfollowing games to use gql endpoints - -2.0.15 -[upd] Update material to reflect repository changes - -2.0.14 -[fix] logging in Kodi 19 - -2.0.13 -[fix] stream language - -2.0.12 -[fix] clip usher - -2.0.11 -[fix] Python 3 - don't decode string when processing clips - -2.0.10 -[upd] allow overriding headers for all hidden api calls - -2.0.9 -[upd] allow overriding headers for usher and hidden api calls -[fix] decode byte responses in scraper -[fix] usher/parser error handling - -2.0.8 -[upd] Mark communities endpoints and queries as deprecated - -2.0.7 -[add] add platform parameter to usher - -2.0.6 -[add] add missing helix api endpoints - -2.0.5 -[add] add frame rate and resolution to returned stream information -[upd] update usher parameters -[upd] use SPDX license identifiers - -2.0.4 -[fix] video/live stream token encoding - -2.0.3 -[chg] redact user ip from token logging - -2.0.2 -[fix] Python 3 compat. -[add] fast_bread param to usher -[add/upd] helix endpoints - -2.0.1 -[fix] clips usher -[chg] to relative imports -[upd] deprecation/removal dates - -2.0.0 -[chg] refactor oauth for inclusion of helix -- adds MobileClient().prepare_token_uri() for generating app access tokens, adds client_secret requirement -- adds MobileClient().prepare_revoke_uri() for revoking oauth tokens -- adds helix oauth scopes -- twitch.scopes -> twitch.oauth..scopes -- twitch.oauth.MobileClient() -> twitch.oauth.clients.MobileClient() -[add] helix api -[add] add usher.live_request and usher.video_request -[add] MobileClient().revoke_token and MobileClient().get_app_access_token queries -[add] Kodi logging -[add] v5 users.users - -1.1.0 -*** Twitch API V5 is deprecated and will be removed entirely on 2/14/18 -*** script.module.python.twitch v2 will include the Twitch helix API, and may include breaking changes to current implementations. - If you rely on this module follow development at https://github.com/MrSprigster/script.module.python.twitch - -[fix/upd] v5.channels deprecated notations for community endpoints, add new communities endpoints - -1.0.2 -[fix] omit oauth token for queries with 'required scope: none' - -1.0.1 -[fix/upd] offset, limit to games _get_followed -[fix] omit oauth token for videos by_id - -1.0.0 -- initial release diff --git a/script.module.python.twitch/resources/__init__.py b/script.module.python.twitch/resources/__init__.py deleted file mode 100644 index a5da79b88..000000000 --- a/script.module.python.twitch/resources/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" diff --git a/script.module.python.twitch/resources/language/resource.language.af_za/strings.po b/script.module.python.twitch/resources/language/resource.language.af_za/strings.po deleted file mode 100644 index f9caee45c..000000000 --- a/script.module.python.twitch/resources/language/resource.language.af_za/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: af_za\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.am_et/strings.po b/script.module.python.twitch/resources/language/resource.language.am_et/strings.po deleted file mode 100644 index 3219e715d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.am_et/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: am_et\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ar_sa/strings.po b/script.module.python.twitch/resources/language/resource.language.ar_sa/strings.po deleted file mode 100644 index 49309ef8d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ar_sa/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ar_sa\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ast_es/strings.po b/script.module.python.twitch/resources/language/resource.language.ast_es/strings.po deleted file mode 100644 index 2c13e49f1..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ast_es/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ast_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.az_az/strings.po b/script.module.python.twitch/resources/language/resource.language.az_az/strings.po deleted file mode 100644 index 7caeff798..000000000 --- a/script.module.python.twitch/resources/language/resource.language.az_az/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: az_az\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.be_by/strings.po b/script.module.python.twitch/resources/language/resource.language.be_by/strings.po deleted file mode 100644 index 94699adb0..000000000 --- a/script.module.python.twitch/resources/language/resource.language.be_by/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: be_by\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.bg_bg/strings.po b/script.module.python.twitch/resources/language/resource.language.bg_bg/strings.po deleted file mode 100644 index ca764b94f..000000000 --- a/script.module.python.twitch/resources/language/resource.language.bg_bg/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: bg_bg\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.bs_ba/strings.po b/script.module.python.twitch/resources/language/resource.language.bs_ba/strings.po deleted file mode 100644 index a8ff29de7..000000000 --- a/script.module.python.twitch/resources/language/resource.language.bs_ba/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: bs_ba\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ca_es/strings.po b/script.module.python.twitch/resources/language/resource.language.ca_es/strings.po deleted file mode 100644 index 40a64bcdf..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ca_es/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ca_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.cs_cz/strings.po b/script.module.python.twitch/resources/language/resource.language.cs_cz/strings.po deleted file mode 100644 index 5efb0628e..000000000 --- a/script.module.python.twitch/resources/language/resource.language.cs_cz/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: cs_cz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.cy_gb/strings.po b/script.module.python.twitch/resources/language/resource.language.cy_gb/strings.po deleted file mode 100644 index 4ed549fe3..000000000 --- a/script.module.python.twitch/resources/language/resource.language.cy_gb/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: cy_gb\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=(n==0) ? 0 : (n==1) ? 1 : (n==2) ? 2 : (n==3) ? 3 :(n==6) ? 4 : 5;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.da_dk/strings.po b/script.module.python.twitch/resources/language/resource.language.da_dk/strings.po deleted file mode 100644 index 1373d1b54..000000000 --- a/script.module.python.twitch/resources/language/resource.language.da_dk/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-07-26 22:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Danish \n" -"Language: da_dk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Modul til interaktion med Twitch.tv API" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch til Kodi er et modul til interaktion med Twitch.tv API baseret på python-twitch af ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.de_de/strings.po b/script.module.python.twitch/resources/language/resource.language.de_de/strings.po deleted file mode 100644 index 984c0409b..000000000 --- a/script.module.python.twitch/resources/language/resource.language.de_de/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-07-26 22:29+0000\n" -"Last-Translator: Kai Sommerfeld \n" -"Language-Team: German \n" -"Language: de_de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Modul für die Interaktion mit der Twitch.tv-API" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch für Kodi ist ein Modul für die Interaktion mit der Twitch.tv-API, basierend auf python-twitch von ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.el_gr/strings.po b/script.module.python.twitch/resources/language/resource.language.el_gr/strings.po deleted file mode 100644 index a8abdb8fc..000000000 --- a/script.module.python.twitch/resources/language/resource.language.el_gr/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: el_gr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.en_au/strings.po b/script.module.python.twitch/resources/language/resource.language.en_au/strings.po deleted file mode 100644 index 538c342e7..000000000 --- a/script.module.python.twitch/resources/language/resource.language.en_au/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: en_au\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.en_gb/strings.po b/script.module.python.twitch/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index 6c97c218a..000000000 --- a/script.module.python.twitch/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE\n" -"Language: en_GB\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.en_nz/strings.po b/script.module.python.twitch/resources/language/resource.language.en_nz/strings.po deleted file mode 100644 index 0bf3c8916..000000000 --- a/script.module.python.twitch/resources/language/resource.language.en_nz/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: en_nz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.en_us/strings.po b/script.module.python.twitch/resources/language/resource.language.en_us/strings.po deleted file mode 100644 index c03bd8b02..000000000 --- a/script.module.python.twitch/resources/language/resource.language.en_us/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: en_us\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.eo/strings.po b/script.module.python.twitch/resources/language/resource.language.eo/strings.po deleted file mode 100644 index 5d972fb41..000000000 --- a/script.module.python.twitch/resources/language/resource.language.eo/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: eo\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.es_ar/strings.po b/script.module.python.twitch/resources/language/resource.language.es_ar/strings.po deleted file mode 100644 index feb15b229..000000000 --- a/script.module.python.twitch/resources/language/resource.language.es_ar/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: es_ar\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.es_es/strings.po b/script.module.python.twitch/resources/language/resource.language.es_es/strings.po deleted file mode 100644 index aadd794fd..000000000 --- a/script.module.python.twitch/resources/language/resource.language.es_es/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-07-03 12:29+0000\n" -"Last-Translator: Alfonso Cachero \n" -"Language-Team: Spanish (Spain) \n" -"Language: es_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7.1\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Módulo para interactuar con la API de Twitch.tv" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch para Kodi es un módulo para interactuar con la API de Twitch.tv basado en python-twitch de ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.es_mx/strings.po b/script.module.python.twitch/resources/language/resource.language.es_mx/strings.po deleted file mode 100644 index 1c570be9e..000000000 --- a/script.module.python.twitch/resources/language/resource.language.es_mx/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: es_mx\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.et_ee/strings.po b/script.module.python.twitch/resources/language/resource.language.et_ee/strings.po deleted file mode 100644 index 1e4e6effa..000000000 --- a/script.module.python.twitch/resources/language/resource.language.et_ee/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: et_ee\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.eu_es/strings.po b/script.module.python.twitch/resources/language/resource.language.eu_es/strings.po deleted file mode 100644 index de8b73ef9..000000000 --- a/script.module.python.twitch/resources/language/resource.language.eu_es/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: eu_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.fa_af/strings.po b/script.module.python.twitch/resources/language/resource.language.fa_af/strings.po deleted file mode 100644 index 5827aa177..000000000 --- a/script.module.python.twitch/resources/language/resource.language.fa_af/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: fa_af\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.fa_ir/strings.po b/script.module.python.twitch/resources/language/resource.language.fa_ir/strings.po deleted file mode 100644 index 9a5629a26..000000000 --- a/script.module.python.twitch/resources/language/resource.language.fa_ir/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: fa_ir\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.fi_fi/strings.po b/script.module.python.twitch/resources/language/resource.language.fi_fi/strings.po deleted file mode 100644 index 59078cb9d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.fi_fi/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-19 06:51+0000\n" -"Last-Translator: Oskari Lavinto \n" -"Language-Team: Finnish \n" -"Language: fi_fi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Moduuli Twitch.tv-rajapinnan käyttöön" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "'Python-twitch for Kodi' on Twitch.tv-palvelun rajapinnan hyödyntämisen mahdollistava moduuli, joka perustuu kehittäjän ingwinlu luomaan python-twitch-moduuliin." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.fo_fo/strings.po b/script.module.python.twitch/resources/language/resource.language.fo_fo/strings.po deleted file mode 100644 index e33bf91a1..000000000 --- a/script.module.python.twitch/resources/language/resource.language.fo_fo/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: fo_fo\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.fr_ca/strings.po b/script.module.python.twitch/resources/language/resource.language.fr_ca/strings.po deleted file mode 100644 index 42403e353..000000000 --- a/script.module.python.twitch/resources/language/resource.language.fr_ca/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: fr_ca\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.fr_fr/strings.po b/script.module.python.twitch/resources/language/resource.language.fr_fr/strings.po deleted file mode 100644 index 8ec67af4a..000000000 --- a/script.module.python.twitch/resources/language/resource.language.fr_fr/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2023-07-18 17:11+0000\n" -"Last-Translator: skypichat \n" -"Language-Team: French (France) \n" -"Language: fr_fr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.18.2\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Module pour interagir avec l'API de Twitch.tv" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch pour Kodi est un module pour interagir avec l'API Twitch.tv basé sur python-twitch de ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.gl_es/strings.po b/script.module.python.twitch/resources/language/resource.language.gl_es/strings.po deleted file mode 100644 index f3b07103b..000000000 --- a/script.module.python.twitch/resources/language/resource.language.gl_es/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: gl_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.he_il/strings.po b/script.module.python.twitch/resources/language/resource.language.he_il/strings.po deleted file mode 100644 index ce7d95aee..000000000 --- a/script.module.python.twitch/resources/language/resource.language.he_il/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: he_il\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && n % 10 == 0) ? 2 : 3));\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.hi_in/strings.po b/script.module.python.twitch/resources/language/resource.language.hi_in/strings.po deleted file mode 100644 index 815f9617d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.hi_in/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: hi_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.hr_hr/strings.po b/script.module.python.twitch/resources/language/resource.language.hr_hr/strings.po deleted file mode 100644 index 5824f81c8..000000000 --- a/script.module.python.twitch/resources/language/resource.language.hr_hr/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2023-01-11 18:15+0000\n" -"Last-Translator: gogogogi \n" -"Language-Team: Croatian \n" -"Language: hr_hr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.15\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Modul za interakciju s Twitch.tv API" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch za Kodi je modul za interakciju s Twitch.tv API-jem temeljenom na python-twitch by ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.hu_hu/strings.po b/script.module.python.twitch/resources/language/resource.language.hu_hu/strings.po deleted file mode 100644 index f55904d37..000000000 --- a/script.module.python.twitch/resources/language/resource.language.hu_hu/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-09-03 07:29+0000\n" -"Last-Translator: Frodo19 \n" -"Language-Team: Hungarian \n" -"Language: hu_hu\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.8\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Modul a Twitch.tv API -val való interakcióhoz" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "A python-twitch a Kodi számára a Twitch.tv API-val való interakció modulja, amely az ingwinlu python-twitch-jén alapul." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.hy_am/strings.po b/script.module.python.twitch/resources/language/resource.language.hy_am/strings.po deleted file mode 100644 index 0731cc165..000000000 --- a/script.module.python.twitch/resources/language/resource.language.hy_am/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: hy_am\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.id_id/strings.po b/script.module.python.twitch/resources/language/resource.language.id_id/strings.po deleted file mode 100644 index 6c224a753..000000000 --- a/script.module.python.twitch/resources/language/resource.language.id_id/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: id_id\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.is_is/strings.po b/script.module.python.twitch/resources/language/resource.language.is_is/strings.po deleted file mode 100644 index ba59c924d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.is_is/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: is_is\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n % 10 != 1 || n % 100 == 11;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.it_it/strings.po b/script.module.python.twitch/resources/language/resource.language.it_it/strings.po deleted file mode 100644 index 3d590cbbc..000000000 --- a/script.module.python.twitch/resources/language/resource.language.it_it/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2023-04-24 04:16+0000\n" -"Last-Translator: Massimo Pissarello \n" -"Language-Team: Italian \n" -"Language: it_it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.15.2\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Modulo per l'interazione con l'API di Twitch.tv" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch per Kodi è un modulo per l'interazione con l'API di Twitch.tv basato su python-twitch di ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ja_jp/strings.po b/script.module.python.twitch/resources/language/resource.language.ja_jp/strings.po deleted file mode 100644 index f86cb144d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ja_jp/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ja_jp\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.kn_in/strings.po b/script.module.python.twitch/resources/language/resource.language.kn_in/strings.po deleted file mode 100644 index 5cd9c36b6..000000000 --- a/script.module.python.twitch/resources/language/resource.language.kn_in/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: kn_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ko_kr/strings.po b/script.module.python.twitch/resources/language/resource.language.ko_kr/strings.po deleted file mode 100644 index fd61f6269..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ko_kr/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-08-28 07:29+0000\n" -"Last-Translator: Minho Park \n" -"Language-Team: Korean \n" -"Language: ko_kr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.8\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Twitch.tv API와의 상호작용을 위한 모듈" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "Kodi용 python-twitch는 ingwinlu의 python-twitch를 기반으로 하는 Twitch.tv API와 상호 작용하기 위한 모듈입니다." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.lt_lt/strings.po b/script.module.python.twitch/resources/language/resource.language.lt_lt/strings.po deleted file mode 100644 index 691509480..000000000 --- a/script.module.python.twitch/resources/language/resource.language.lt_lt/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: lt_lt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n % 10 == 1 && (n % 100 < 11 || n % 100 > 19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.lv_lv/strings.po b/script.module.python.twitch/resources/language/resource.language.lv_lv/strings.po deleted file mode 100644 index 29af1b6ee..000000000 --- a/script.module.python.twitch/resources/language/resource.language.lv_lv/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: lv_lv\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.mi/strings.po b/script.module.python.twitch/resources/language/resource.language.mi/strings.po deleted file mode 100644 index 8139e5f72..000000000 --- a/script.module.python.twitch/resources/language/resource.language.mi/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.mk_mk/strings.po b/script.module.python.twitch/resources/language/resource.language.mk_mk/strings.po deleted file mode 100644 index 9d585c39d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.mk_mk/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mk_mk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n==1 || n%10==1 ? 0 : 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ml_in/strings.po b/script.module.python.twitch/resources/language/resource.language.ml_in/strings.po deleted file mode 100644 index 91be51d80..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ml_in/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ml_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.mn_mn/strings.po b/script.module.python.twitch/resources/language/resource.language.mn_mn/strings.po deleted file mode 100644 index 1ab5565a7..000000000 --- a/script.module.python.twitch/resources/language/resource.language.mn_mn/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mn_mn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ms_my/strings.po b/script.module.python.twitch/resources/language/resource.language.ms_my/strings.po deleted file mode 100644 index b3e645527..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ms_my/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ms_my\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.mt_mt/strings.po b/script.module.python.twitch/resources/language/resource.language.mt_mt/strings.po deleted file mode 100644 index cb4f4c601..000000000 --- a/script.module.python.twitch/resources/language/resource.language.mt_mt/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mt_mt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.my_mm/strings.po b/script.module.python.twitch/resources/language/resource.language.my_mm/strings.po deleted file mode 100644 index 2403bdb4b..000000000 --- a/script.module.python.twitch/resources/language/resource.language.my_mm/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: my_mm\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.nb_no/strings.po b/script.module.python.twitch/resources/language/resource.language.nb_no/strings.po deleted file mode 100644 index 23d1b7bc8..000000000 --- a/script.module.python.twitch/resources/language/resource.language.nb_no/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: nb_no\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.nl_nl/strings.po b/script.module.python.twitch/resources/language/resource.language.nl_nl/strings.po deleted file mode 100644 index 467771de3..000000000 --- a/script.module.python.twitch/resources/language/resource.language.nl_nl/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: nl_nl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.oc_fr/strings.po b/script.module.python.twitch/resources/language/resource.language.oc_fr/strings.po deleted file mode 100644 index 345650300..000000000 --- a/script.module.python.twitch/resources/language/resource.language.oc_fr/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: oc_fr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.os_os/strings.po b/script.module.python.twitch/resources/language/resource.language.os_os/strings.po deleted file mode 100644 index 68122aecd..000000000 --- a/script.module.python.twitch/resources/language/resource.language.os_os/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: os_os\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.pl_pl/strings.po b/script.module.python.twitch/resources/language/resource.language.pl_pl/strings.po deleted file mode 100644 index 28d29c3dd..000000000 --- a/script.module.python.twitch/resources/language/resource.language.pl_pl/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-07-16 19:29+0000\n" -"Last-Translator: Marek Adamski \n" -"Language-Team: Polish \n" -"Language: pl_pl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.7.1\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Moduł do interakcji z API Twitch.tv" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch dla Kodi to moduł do interakcji z API Twitch.tv oparty na python-twitch autorstwa ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.pt_br/strings.po b/script.module.python.twitch/resources/language/resource.language.pt_br/strings.po deleted file mode 100644 index 2a0673776..000000000 --- a/script.module.python.twitch/resources/language/resource.language.pt_br/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: pt_br\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.pt_pt/strings.po b/script.module.python.twitch/resources/language/resource.language.pt_pt/strings.po deleted file mode 100644 index 1149431b3..000000000 --- a/script.module.python.twitch/resources/language/resource.language.pt_pt/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: pt_pt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ro_ro/strings.po b/script.module.python.twitch/resources/language/resource.language.ro_ro/strings.po deleted file mode 100644 index a215c771e..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ro_ro/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ro_ro\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ru_ru/strings.po b/script.module.python.twitch/resources/language/resource.language.ru_ru/strings.po deleted file mode 100644 index c94f879d0..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ru_ru/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-06-25 00:29+0000\n" -"Last-Translator: Dmitry Petrov \n" -"Language-Team: Russian \n" -"Language: ru_ru\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.7\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "Модуль для взаимодействия с Twitch.tv API" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "python-twitch это модуль для взаимодействия с Twitch.tv API. Основан на модуле python-twitch от ingwinlu." - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.si_lk/strings.po b/script.module.python.twitch/resources/language/resource.language.si_lk/strings.po deleted file mode 100644 index 669f34d61..000000000 --- a/script.module.python.twitch/resources/language/resource.language.si_lk/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: si_lk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.sk_sk/strings.po b/script.module.python.twitch/resources/language/resource.language.sk_sk/strings.po deleted file mode 100644 index 4289d32cc..000000000 --- a/script.module.python.twitch/resources/language/resource.language.sk_sk/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: sk_sk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.sl_si/strings.po b/script.module.python.twitch/resources/language/resource.language.sl_si/strings.po deleted file mode 100644 index e2c508998..000000000 --- a/script.module.python.twitch/resources/language/resource.language.sl_si/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: sl_si\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.sq_al/strings.po b/script.module.python.twitch/resources/language/resource.language.sq_al/strings.po deleted file mode 100644 index f6ec84b8a..000000000 --- a/script.module.python.twitch/resources/language/resource.language.sq_al/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: sq_al\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.sr_rs/strings.po b/script.module.python.twitch/resources/language/resource.language.sr_rs/strings.po deleted file mode 100644 index 93395c690..000000000 --- a/script.module.python.twitch/resources/language/resource.language.sr_rs/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: sr_rs\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.sr_rs@latin/strings.po b/script.module.python.twitch/resources/language/resource.language.sr_rs@latin/strings.po deleted file mode 100644 index a25e3cefd..000000000 --- a/script.module.python.twitch/resources/language/resource.language.sr_rs@latin/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: sr_Latn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.sv_se/strings.po b/script.module.python.twitch/resources/language/resource.language.sv_se/strings.po deleted file mode 100644 index e6465e747..000000000 --- a/script.module.python.twitch/resources/language/resource.language.sv_se/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: sv_se\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.szl/strings.po b/script.module.python.twitch/resources/language/resource.language.szl/strings.po deleted file mode 100644 index 237d4790f..000000000 --- a/script.module.python.twitch/resources/language/resource.language.szl/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: szl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.ta_in/strings.po b/script.module.python.twitch/resources/language/resource.language.ta_in/strings.po deleted file mode 100644 index 85acf5618..000000000 --- a/script.module.python.twitch/resources/language/resource.language.ta_in/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ta_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.te_in/strings.po b/script.module.python.twitch/resources/language/resource.language.te_in/strings.po deleted file mode 100644 index af21ffcd6..000000000 --- a/script.module.python.twitch/resources/language/resource.language.te_in/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: te_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.tg_tj/strings.po b/script.module.python.twitch/resources/language/resource.language.tg_tj/strings.po deleted file mode 100644 index 3a3a0147e..000000000 --- a/script.module.python.twitch/resources/language/resource.language.tg_tj/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: tg_tj\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.th_th/strings.po b/script.module.python.twitch/resources/language/resource.language.th_th/strings.po deleted file mode 100644 index b57ce775d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.th_th/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: th_th\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.tr_tr/strings.po b/script.module.python.twitch/resources/language/resource.language.tr_tr/strings.po deleted file mode 100644 index c623f4a6a..000000000 --- a/script.module.python.twitch/resources/language/resource.language.tr_tr/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: tr_tr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.uk_ua/strings.po b/script.module.python.twitch/resources/language/resource.language.uk_ua/strings.po deleted file mode 100644 index 6d43b0223..000000000 --- a/script.module.python.twitch/resources/language/resource.language.uk_ua/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: uk_ua\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.uz_uz/strings.po b/script.module.python.twitch/resources/language/resource.language.uz_uz/strings.po deleted file mode 100644 index 9b19a1063..000000000 --- a/script.module.python.twitch/resources/language/resource.language.uz_uz/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: uz_uz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.vi_vn/strings.po b/script.module.python.twitch/resources/language/resource.language.vi_vn/strings.po deleted file mode 100644 index ac7fb232d..000000000 --- a/script.module.python.twitch/resources/language/resource.language.vi_vn/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: vi_vn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.zh_cn/strings.po b/script.module.python.twitch/resources/language/resource.language.zh_cn/strings.po deleted file mode 100644 index 13e218865..000000000 --- a/script.module.python.twitch/resources/language/resource.language.zh_cn/strings.po +++ /dev/null @@ -1,26 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-11-09 03:30+0000\n" -"Last-Translator: taxigps \n" -"Language-Team: Chinese (China) \n" -"Language: zh_cn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.8.1\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "与 Twitch.tv API 交互的模块" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "Kodi 的 python-twitch 是从 ingwinlu 开发版本移植的与 twitch.tv API 交互的模块。" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/language/resource.language.zh_tw/strings.po b/script.module.python.twitch/resources/language/resource.language.zh_tw/strings.po deleted file mode 100644 index 4e24d8b33..000000000 --- a/script.module.python.twitch/resources/language/resource.language.zh_tw/strings.po +++ /dev/null @@ -1,25 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: zh_tw\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "Module for interaction with the Twitch.tv API" -msgstr "" - -msgctxt "Addon Description" -msgid "python-twitch for Kodi is module for interaction with the Twitch.tv API based on python-twitch by ingwinlu." -msgstr "" - -# msgctxt "#30000" -# msgid "" -# msgstr "" diff --git a/script.module.python.twitch/resources/lib/__init__.py b/script.module.python.twitch/resources/lib/__init__.py deleted file mode 100644 index af6ec4489..000000000 --- a/script.module.python.twitch/resources/lib/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -__all__ = ['twitch'] - -from . import twitch diff --git a/script.module.python.twitch/resources/lib/twitch/__init__.py b/script.module.python.twitch/resources/lib/twitch/__init__.py deleted file mode 100644 index 9daac67ff..000000000 --- a/script.module.python.twitch/resources/lib/twitch/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -__all__ = ['VERSION', 'CLIENT_ID', 'CLIENT_SECRET', 'OAUTH_TOKEN', 'APP_TOKEN', 'api', 'oauth', - 'exceptions', 'keys', 'log', 'methods', 'parser', 'queries', 'scraper'] - -VERSION = '2.0.0' -CLIENT_ID = '' -CLIENT_SECRET = '' -OAUTH_TOKEN = '' -APP_TOKEN = '' - -from . import api -from . import oauth -from . import exceptions -from . import keys -from . import log -from . import methods -from . import parser -from . import queries -from . import scraper diff --git a/script.module.python.twitch/resources/lib/twitch/api/__init__.py b/script.module.python.twitch/resources/lib/twitch/api/__init__.py deleted file mode 100644 index 2eff873f6..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -__all__ = ['default', 'helix', 'parameters', 'usher'] - -from . import helix as default -from . import helix -from . import parameters -from . import usher diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/__init__.py b/script.module.python.twitch/resources/lib/twitch/api/helix/__init__.py deleted file mode 100644 index 2a5eeb12a..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/ - - Copyright (C) 2016-2019 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -__all__ = ['analytics', 'bits', 'clips', 'entitlements', 'games', 'search', 'streams', - 'subscriptions', 'tags', 'users', 'videos', 'webhooks'] - -from . import analytics # NOQA -from . import bits # NOQA -from . import clips # NOQA -from . import entitlements # NOQA -from . import games # NOQA -from . import search # NOQA -from . import streams # NOQA -from . import subscriptions # NOQA -from . import tags # NOQA -from . import users # NOQA -from . import videos # NOQA -from . import webhooks # NOQA diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/analytics.py b/script.module.python.twitch/resources/lib/twitch/api/helix/analytics.py deleted file mode 100644 index 15ca0006e..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/analytics.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..parameters import IntRange, Cursor, ReportType -from ... import keys -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: analytics:read:extensions -@query -def extensions(started_at='', ended_at='', extension_id='', report_type='', after='MA==', first=20, use_app_token=False): - q = Qry('analytics/extensions', use_app_token=use_app_token) - q.add_param(keys.STARTED_AT, started_at, '') - q.add_param(keys.ENDED_AT, ended_at, '') - q.add_param(keys.EXTENSION_ID, extension_id, '') - if report_type: - q.add_param(keys.TYPE, ReportType.validate(report_type)) - if not extension_id: - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q - - -# required scope: analytics:read:games -@query -def games(started_at='', ended_at='', game_id='', report_type='', after='MA==', first=20, use_app_token=False): - q = Qry('analytics/games', use_app_token=use_app_token) - q.add_param(keys.STARTED_AT, started_at, '') - q.add_param(keys.ENDED_AT, ended_at, '') - q.add_param(keys.GAME_ID, game_id, '') - if report_type: - q.add_param(keys.TYPE, ReportType.validate(report_type)) - if not game_id: - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/bits.py b/script.module.python.twitch/resources/lib/twitch/api/helix/bits.py deleted file mode 100644 index da9481724..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/bits.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..parameters import PeriodHelix, IntRange -from ... import keys -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: bits:read -@query -def get_bits_leaderboard(count=10, period=PeriodHelix.ALL, started_at='', user_id='', use_app_token=False): - q = Qry('bits/leaderboard', use_app_token=use_app_token) - q.add_param(keys.COUNT, IntRange(1, 100).validate(count), 10) - q.add_param(keys.PERIOD, PeriodHelix.validate(period), PeriodHelix.ALL) - if period != PeriodHelix.ALL: - q.add_param(keys.STARTED_AT, started_at, '') - q.add_param(keys.USER_ID, user_id, '') - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/clips.py b/script.module.python.twitch/resources/lib/twitch/api/helix/clips.py deleted file mode 100644 index af1a0902e..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/clips.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..parameters import Boolean, Cursor, IntRange, ItemCount -from ... import keys, methods -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: none -@query -def get_clip(broadcaster_id='', game_id='', clip_id=list(), - after='MA==', before='MA==', first=20, use_app_token=False): - q = Qry('clips', use_app_token=use_app_token) - q.add_param(keys.ID, ItemCount().validate(clip_id), list()) - if len(clip_id) != 1: - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.BEFORE, Cursor.validate(before), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - q.add_param(keys.BROADCASTER_ID, broadcaster_id, '') - q.add_param(keys.GAME_ID, game_id, '') - q.add_param(keys.ID, ItemCount().validate(clip_id), list()) - - return q - - -# required scope: clips:edit -@query -def create_clip(broadcaster_id, has_delay=Boolean.FALSE, use_app_token=False): - q = Qry('clips', use_app_token=use_app_token, method=methods.POST) - q.add_param(keys.BROADCASTER_ID, broadcaster_id, '') - q.add_param(keys.HAS_DELAY, Boolean.validate(has_delay), Boolean.FALSE) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/entitlements.py b/script.module.python.twitch/resources/lib/twitch/api/helix/entitlements.py deleted file mode 100644 index e146ad427..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/entitlements.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2019 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..parameters import EntitlementType, ItemCount -from ... import keys -from ... import methods -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: none -# requires app access token -@query -def upload(manifest_id, entitlement_type=EntitlementType.BULK_DROPS_GRANT): - q = Qry('entitlements/upload', use_app_token=True) - q.add_param(keys.MANIFEST_ID, manifest_id) - q.add_param(keys.TYPE, EntitlementType.validate(entitlement_type)) - - return q - - -# required scope: none -# requires app access token -@query -def get_code_status(code, user_id): - q = Qry('entitlements/codes', use_app_token=True, method=methods.GET) - q.add_param(keys.CODE, ItemCount(max_items=20).validate(code), list()) - q.add_param(keys.USER_ID, user_id) - - return q - - -# required scope: none -# requires app access token -@query -def redeem_code(code, user_id): - q = Qry('entitlements/codes', use_app_token=True, method=methods.POST) - q.add_param(keys.CODE, ItemCount(max_items=20).validate(code), list()) - q.add_param(keys.USER_ID, user_id) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/games.py b/script.module.python.twitch/resources/lib/twitch/api/helix/games.py deleted file mode 100644 index 19238fcb0..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/games.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ... import keys -from ...api.parameters import Cursor, IntRange, ItemCount -from ...queries import HelixQuery as Qry -from ...queries import GQLQuery as GQLQry -from ...queries import HiddenApiQuery as HQry -from ...queries import query - - -# required scope: none -@query -def get_games(game_id=list(), game_name=list(), use_app_token=False): - q = Qry('games', use_app_token=use_app_token) - q.add_param(keys.ID, ItemCount().validate(game_id), list()) - q.add_param(keys.NAME, ItemCount().validate(game_name), list()) - - return q - - -# required scope: none -@query -def get_top(after='MA==', before='MA==', first=20, use_app_token=False): - q = Qry('games/top', use_app_token=use_app_token) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.BEFORE, Cursor.validate(before), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q - - -# required scope: none -# undocumented / unsupported -@query -def _check_follows(username, name, headers={}): - q = HQry('users/{username}/follows/games/isFollowing', headers=headers, use_token=False) - q.add_urlkw(keys.USERNAME, username) - q.add_param(keys.NAME, name) - return q - - -# required scope: none -# undocumented / unsupported -@query -def _get_followed(limit=100, headers={}): - data = [{ - "operationName": "FollowingGames_CurrentUser", - "variables": { - "limit": limit, - "type": "LIVE" - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "8446d4d234005813dc1f024f487ce95434c3e4202f451dd42777935b5ed035ce" - } - } - }] - q = GQLQry('', headers=headers, data=data, use_token=False) - return q - - -# required scope: user_follows_edit -# undocumented / unsupported -@query -def _follow(game_id, headers={}): - data = [{ - "operationName": "FollowGameButton_FollowGame", - "variables": { - "input": { - "gameID": str(game_id) - } - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "b846b65ba4bc9a3561dbe2d069d95deed9b9e031bcfda2482d1bedd84a1c2eb3" - } - } - }] - q = GQLQry('', headers=headers, data=data, use_token=False) - return q - - -# required scope: user_follows_edit -# undocumented / unsupported -@query -def _unfollow(game_id, headers={}): - data = [{ - "operationName": "FollowGameButton_UnfollowGame", - "variables": { - "input": { - "gameID": str(game_id) - } - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "811e02e396ebba0664f21ff002f2eff3c6f57e8af9aedb4f4dfa77cefd0db43d" - } - } - }] - q = GQLQry('', headers=headers, data=data, use_token=False) - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/search.py b/script.module.python.twitch/resources/lib/twitch/api/helix/search.py deleted file mode 100644 index 7819e2392..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/search.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2022- script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ... import keys -from ...api.parameters import Boolean, Cursor, IntRange -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: none -@query -def get_categories(search_query, after='MA==', first=20, use_app_token=False): - q = Qry('search/categories', use_app_token=use_app_token) - q.add_param(keys.QUERY, search_query) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q - - -# required scope: none -@query -def get_channels(search_query, after='MA==', first=20, live_only=True, use_app_token=False): - q = Qry('search/channels', use_app_token=use_app_token) - q.add_param(keys.QUERY, search_query) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - q.add_param(keys.LIVE_ONLY, Boolean.validate(live_only), Boolean.FALSE) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/streams.py b/script.module.python.twitch/resources/lib/twitch/api/helix/streams.py deleted file mode 100644 index b3b526830..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/streams.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2019 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ... import keys -from ...api.parameters import Cursor, Language, IntRange, ItemCount -from ... import methods -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: none -@query -def get_streams(game_id=list(), user_id=list(), - user_login=list(), language=list(), after='MA==', - before='MA==', first=20, use_app_token=False): - q = Qry('streams', use_app_token=use_app_token) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.BEFORE, Cursor.validate(before), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - q.add_param(keys.GAME_ID, ItemCount().validate(game_id), list()) - q.add_param(keys.USER_ID, ItemCount().validate(user_id), list()) - q.add_param(keys.USER_LOGIN, ItemCount().validate(user_login), list()) - if isinstance(language, list): - _language = [lang for lang in language if lang in Language.valid()] - q.add_param(keys.LANGUAGE, ItemCount().validate(_language), list()) - else: - q.add_param(keys.LANGUAGE, Language.validate(language), '') - - return q - - -# required scope: user:read:follows -@query -def get_followed(user_id, after='MA==', first=20, use_app_token=False): - q = Qry('streams/followed', use_app_token=use_app_token) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - q.add_param(keys.USER_ID, user_id) - - return q - - -# required scope: none -@query -def get_metadata(game_id=list(), user_id=list(), - user_login=list(), language=list(), after='MA==', - before='MA==', first=20, use_app_token=False): - q = Qry('streams/metadata', use_app_token=use_app_token) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.BEFORE, Cursor.validate(before), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - q.add_param(keys.GAME_ID, ItemCount().validate(game_id), list()) - q.add_param(keys.USER_ID, ItemCount().validate(user_id), list()) - q.add_param(keys.USER_LOGIN, ItemCount().validate(user_login), list()) - if isinstance(language, list): - _language = [lang for lang in language if lang in Language.valid()] - q.add_param(keys.LANGUAGE, ItemCount().validate(_language), list()) - else: - q.add_param(keys.LANGUAGE, Language.validate(language), '') - - return q - - -# required scope: user:edit:broadcast -@query -def create_stream_marker(user_id, description=''): - q = Qry('streams/markers', use_app_token=False, method=methods.POST) - q.add_param(keys.USER_ID, user_id) - q.add_param(keys.DESCRIPTION, description, '') - - return q - - -# required scope: user:read:broadcast -@query -def get_stream_markers(user_id, video_id, after='MA==', before='MA==', first=20): - q = Qry('streams/markers', use_app_token=False, method=methods.GET) - q.add_param(keys.USER_ID, user_id) - q.add_param(keys.VIDEO_ID, video_id) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.BEFORE, Cursor.validate(before), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/subscriptions.py b/script.module.python.twitch/resources/lib/twitch/api/helix/subscriptions.py deleted file mode 100644 index 5b23843e6..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/subscriptions.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2019 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..parameters import Cursor, IntRange, ItemCount -from ... import keys, methods -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: channel:read:subscriptions -@query -def get_broadcaster_subscriptions(broadcaster_id, user_id=list(), after='MA==', first=20): - q = Qry('subscriptions', use_app_token=False, method=methods.GET) - q.add_param(keys.BROADCASTER_ID, broadcaster_id) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - q.add_param(keys.USER_ID, ItemCount().validate(user_id), list()) - return q - - -# required scope: channel:read:subscriptions -@query -def get_user_subscriptions(broadcaster_id, user_id): - q = Qry('subscriptions/user', use_app_token=False, method=methods.GET) - q.add_param(keys.BROADCASTER_ID, broadcaster_id) - q.add_param(keys.USER_ID, ItemCount().validate(user_id), list()) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/tags.py b/script.module.python.twitch/resources/lib/twitch/api/helix/tags.py deleted file mode 100644 index aa9d0225f..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/tags.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2019 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..parameters import Cursor, IntRange, ItemCount -from ... import keys, methods -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: none -# requires app access token -@query -def get_all_stream_tags(tag_id, after='MA==', first=20): - q = Qry('tags/streams', use_app_token=True, method=methods.GET) - q.add_param(keys.TAG_ID, ItemCount().validate(tag_id), list()) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q - - -# required scope: none -# requires app access token -@query -def get_stream_tags(broadcaster_id): - q = Qry('streams/tags', use_app_token=True, method=methods.GET) - q.add_param(keys.BROADCASTER_ID, broadcaster_id) - - return q - - -# required scope: user:edit:broadcast -@query -def replace_stream_tags(broadcaster_id, tag_ids=list()): - q = Qry('tags/streams', use_app_token=False, method=methods.PUT) - q.add_param(keys.BROADCASTER_ID, broadcaster_id) - q.add_param(keys.TAG_IDS, ItemCount().validate(tag_ids), list()) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/users.py b/script.module.python.twitch/resources/lib/twitch/api/helix/users.py deleted file mode 100644 index 4da035df9..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/users.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ... import keys, methods -from ...api.parameters import Cursor, IntRange, ItemCount -from ...queries import GQLQuery as GQLQry -from ...queries import HelixQuery as Qry -from ...queries import query - - -# optional scope: user:read:email -@query -def get_users(user_id=list(), user_login=list(), use_app_token=False): - use_token = (not user_id and not user_login) - use_app_token = False if use_token else use_app_token - q = Qry('users', use_app_token=use_app_token) - q.add_param(keys.ID, ItemCount().validate(user_id), list()) - q.add_param(keys.LOGIN, ItemCount().validate(user_login), list()) - - return q - - -# required scope: none -@query -def get_follows(user_id='', after='MA==', first=20, use_app_token=False): - q = Qry('channels/followed', use_app_token=use_app_token) - q.add_param(keys.USER_ID, user_id, '') - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q - - -# required scope: user:edit -@query -def put_users(description): - q = Qry('users', method=methods.PUT) - q.add_param(keys.DESCRIPTION, description, '') - - return q - - -# required scope: user:read:broadcast -@query -def get_extensions(): - q = Qry('users/extensions/list') - - return q - - -# optional scope: user:read:broadcast or user:edit:broadcast -@query -def get_active_extensions(user_id=''): - q = Qry('users/extensions') - q.add_param(keys.USER_ID, user_id, '') - - return q - - -# required scope: user:edit:broadcast -@query -def update_extensions(): - q = Qry('users/extensions', method=methods.PUT) - - return q - - -# required scope: user_follows_edit -@query -def _follow_channel(channel_id, headers={}, notifications=False): - data = [{ - "operationName": "FollowButton_FollowUser", - "variables": { - "input": { - "disableNotifications": notifications, - "targetID": str(channel_id) - } - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "14319edb840c1dfce880dc64fa28a1f4eb69d821901e9e96eb9610d2e52b54f2" - } - } - }] - q = GQLQry('', headers=headers, data=data, use_token=False) - return q - - -# required scope: user_follows_edit -@query -def _unfollow_channel(channel_id, headers={}): - data = [{ - "operationName": "FollowButton_UnfollowUser", - "variables": { - "input": { - "targetID": str(channel_id) - } - }, - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "29783a1dac24124e02f7295526241a9f1476cd2f5ce1e394f93ea50c253d8628" - } - } - }] - q = GQLQry('', headers=headers, data=data, use_token=False) - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/videos.py b/script.module.python.twitch/resources/lib/twitch/api/helix/videos.py deleted file mode 100644 index cf3c3be78..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/videos.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ... import keys -from ...api.parameters import Cursor, Language, BroadcastTypeHelix, VideoSortHelix, PeriodHelix, IntRange, ItemCount -from ...queries import HelixQuery as Qry -from ...queries import HiddenApiQuery as HQry -from ...queries import query - - -# required scope: none -@query -def get_videos(video_id=list(), game_id='', user_id='', - broadcast_type=BroadcastTypeHelix.ALL, language='', - after='MA==', before='MA==', first=20, - sort_order=VideoSortHelix.TIME, period=PeriodHelix.ALL, use_app_token=False): - q = Qry('videos', use_app_token=use_app_token) - if not video_id: - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.BEFORE, Cursor.validate(before), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - q.add_param(keys.GAME_ID, game_id, '') - q.add_param(keys.USER_ID, user_id, '') - q.add_param(keys.TYPE, BroadcastTypeHelix.validate(broadcast_type), BroadcastTypeHelix.ALL) - q.add_param(keys.SORT, VideoSortHelix.validate(sort_order), VideoSortHelix.TIME) - q.add_param(keys.PERIOD, PeriodHelix.validate(period), PeriodHelix.ALL) - if language: - q.add_param(keys.LANGUAGE, Language.validate(language), '') - else: - q.add_param(keys.ID, ItemCount().validate(video_id), list()) - - return q - - -# required scope: none -# undocumented / unsupported -@query -def _by_id(video_id, headers={}): - q = HQry('videos/{video_id}', headers=headers, use_token=False) - q.add_urlkw(keys.VIDEO_ID, video_id) - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/helix/webhooks.py b/script.module.python.twitch/resources/lib/twitch/api/helix/webhooks.py deleted file mode 100644 index bfe92498f..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/helix/webhooks.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/api/reference - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..parameters import Cursor, IntRange -from ... import keys -from ...queries import HelixQuery as Qry -from ...queries import query - - -# required scope: none -# requires app access token -@query -def subscriptions(after='MA==', first=20): - q = Qry('webhooks/subscriptions', use_app_token=True) - q.add_param(keys.AFTER, Cursor.validate(after), 'MA==') - q.add_param(keys.FIRST, IntRange(1, 100).validate(first), 20) - - return q diff --git a/script.module.python.twitch/resources/lib/twitch/api/parameters.py b/script.module.python.twitch/resources/lib/twitch/api/parameters.py deleted file mode 100644 index d5be526e9..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/parameters.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from base64 import b64decode - - -class _Parameter(object): - _valid = [] - - @classmethod - def valid(cls): - return cls._valid - - @classmethod - def validate(cls, value): - if value in cls._valid: - return value - raise ValueError(value) - - -class Period(_Parameter): - WEEK = 'week' - MONTH = 'month' - ALL = 'all' - _valid = [WEEK, MONTH, ALL] - - -class PeriodHelix(_Parameter): - DAY = 'day' - WEEK = 'week' - MONTH = 'month' - ALL = 'all' - _valid = [DAY, WEEK, MONTH, ALL] - - -class ClipPeriod(_Parameter): - DAY = 'day' - WEEK = 'week' - MONTH = 'month' - ALL = 'all' - _valid = [DAY, WEEK, MONTH, ALL] - - -class Boolean(_Parameter): - TRUE = 'true' - FALSE = 'false' - - _valid = [TRUE, FALSE] - - -class Direction(_Parameter): - DESC = 'desc' - ASC = 'asc' - - _valid = [DESC, ASC] - - -class SortBy(_Parameter): - CREATED_AT = 'created_at' - LAST_BROADCAST = 'last_broadcast' - LOGIN = 'login' - - _valid = [CREATED_AT, LAST_BROADCAST, LOGIN] - - -class VideoSort(_Parameter): - VIEWS = 'views' - TIME = 'time' - - _valid = [VIEWS, TIME] - - -class VideoSortHelix(_Parameter): - VIEWS = 'views' - TIME = 'time' - TRENDING = 'trending' - - _valid = [VIEWS, TIME, TRENDING] - - -class BroadcastType(_Parameter): - ARCHIVE = 'archive' - HIGHLIGHT = 'highlight' - UPLOAD = 'upload' - - _valid = [ARCHIVE, HIGHLIGHT, UPLOAD] - - @classmethod - def validate(cls, value): - split_values = value.split(',') - for val in split_values: - if val not in cls._valid: - raise ValueError(value) - return value - - -class BroadcastTypeHelix(_Parameter): - ARCHIVE = 'archive' - HIGHLIGHT = 'highlight' - UPLOAD = 'upload' - ALL = 'all' - - _valid = [ALL, ARCHIVE, HIGHLIGHT, UPLOAD] - - @classmethod - def validate(cls, value): - split_values = value.split(',') - for val in split_values: - if val not in cls._valid: - raise ValueError(value) - return value - - -class StreamType(_Parameter): - LIVE = 'live' - PLAYLIST = 'playlist' - ALL = 'all' - - _valid = [LIVE, PLAYLIST, ALL] - - -class StreamTypeHelix(_Parameter): - LIVE = 'live' - VODCAST = 'vodcast' - ALL = 'all' - - _valid = [LIVE, VODCAST, ALL] - - -class Platform(_Parameter): - XBOX_ONE = 'xbox_one' - PS4 = 'ps4' - ALL = 'all' - - _valid = [XBOX_ONE, PS4, ALL] - - -class Cursor(_Parameter): - @classmethod - def validate(cls, value): - try: - padding = (4 - len(value) % 4) % 4 - padding *= '=' - decoded = b64decode(value + padding) - return value - except ValueError: - raise ValueError(value) - - -class Language(_Parameter): - ALL = '' - EN = 'en' - DA = 'da' - DE = 'de' - ES = 'es' - FR = 'fr' - IT = 'it' - HU = 'hu' - NL = 'nl' - NO = 'no' - PL = 'pl' - OTHER = 'other' - ASL = 'asl' - KO = 'ko' - JA = 'ja' - ZH = 'zh' - TH = 'th' - AR = 'ar' - RU = 'ru' - BG = 'bg' - EL = 'el' - CS = 'cs' - TR = 'tr' - VI = 'vi' - SV = 'sv' - FI = 'fi' - SK = 'sk' - PT = 'pt' - - _valid = [ALL, EN, DA, DE, ES, FR, IT, HU, NL, - NO, PL, OTHER, ASL, KO, JA, ZH, TH, - AR, RU, BG, EL, CS, TR, VI, SV, FI, - SK, PT] - - @classmethod - def validate(cls, value): - if value not in cls._valid: - raise ValueError(value) - return value - - -class Duration(_Parameter): - _valid = [30, 60, 90, 120, 150, 180] - - -class ReportType(_Parameter): - OVERVIEW_V1 = 'overview_v1' - OVERVIEW_V2 = 'overview_v2' - - _valid = [OVERVIEW_V1, OVERVIEW_V2] - - -class EntitlementType(_Parameter): - BULK_DROPS_GRANT = 'bulk_drops_grant' - - _valid = [BULK_DROPS_GRANT] - - -class IntRange(_Parameter): - - @classmethod - def __init__(cls, first, last): - cls._valid = [i for i in range(first, last + 1)] - - -class ItemCount(object): - _max_items = 100 - - @classmethod - def __init__(cls, max_items=100): - cls._max_items = max_items - - @classmethod - def valid(cls): - raise NotImplementedError - - @classmethod - def validate(cls, value): - if len(value) <= cls._max_items: - return value - raise ValueError(value) diff --git a/script.module.python.twitch/resources/lib/twitch/api/usher.py b/script.module.python.twitch/resources/lib/twitch/api/usher.py deleted file mode 100644 index b03edff77..000000000 --- a/script.module.python.twitch/resources/lib/twitch/api/usher.py +++ /dev/null @@ -1,251 +0,0 @@ -# -*- encoding: utf-8 -*- -# By using this module you are violating the Twitch TOS -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -import json -from urllib.parse import urlencode - -from .. import keys -from ..api.parameters import Boolean -from ..parser import m3u8, clip_embed -from ..queries import ClipsQuery, HiddenApiQuery, UsherQuery, GQLQuery -from ..queries import query -from ..log import log - -ACCESS_TOKEN_EXCEPTION = { - 'error': 'Error', - 'message': 'Failed to retrieve access token', - 'status': 404 -} - - -def get_access_token(token): - stream_access_token = None - video_access_token = None - if isinstance(token, list): - if token: - data = token[0].get(keys.DATA, {}) - stream_access_token = data.get(keys.STREAM_PLAYBACK_ACCESS_TOKEN) - video_access_token = data.get(keys.VIDEO_PLAYBACK_ACCESS_TOKEN) - return stream_access_token or video_access_token or token - - -def valid_video_id(video_id): - if video_id.startswith('videos'): - video_id = 'v' + video_id[6:] - if video_id.startswith(('a', 'c', 'v')): - return video_id[1:] - return video_id - - -@query -def channel_token(channel, platform=keys.WEB, headers={}): - data = [{ - "operationName": "PlaybackAccessToken_Template", - "query": "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}", - "variables": { - "isLive": True, - "login": channel, - "isVod": False, - "vodID": "", - "playerType": "site" - } - }] - q = GQLQuery('', headers=headers, data=data, use_token=True) - return q - - -@query -def vod_token(video_id, platform=keys.WEB, headers={}): - data = [{ - "operationName": "PlaybackAccessToken_Template", - "query": "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}", - "variables": { - "isLive": False, - "login": "", - "isVod": True, - "vodID": video_id, - "playerType": "site" - } - }] - q = GQLQuery('', headers=headers, data=data, use_token=True) - return q - - -@query -def _legacy_video(video_id): - q = HiddenApiQuery('videos/{id}') - q.add_urlkw(keys.ID, video_id) - return q - - -def live_request(channel, platform=keys.WEB, headers={}): - token = channel_token(channel, platform=platform, headers=headers) - token = get_access_token(token) - - if not token: - return ACCESS_TOKEN_EXCEPTION - elif isinstance(token, dict) and 'error' in token: - return token - else: - signature = token[keys.SIGNATURE] - access_token = token[keys.VALUE] - q = UsherQuery('api/channel/hls/{channel}.m3u8', headers=headers) - q.add_urlkw(keys.CHANNEL, channel) - q.add_param(keys.SIG, signature.encode('utf-8')) - q.add_param(keys.TOKEN, access_token.encode('utf-8')) - q.add_param(keys.ALLOW_SOURCE, Boolean.TRUE) - q.add_param(keys.ALLOW_SPECTRE, Boolean.TRUE) - q.add_param(keys.ALLOW_AUDIO_ONLY, Boolean.TRUE) - q.add_param(keys.FAST_BREAD, Boolean.TRUE) - q.add_param(keys.CDM, keys.WV) - q.add_param(keys.REASSIGNMENT_SUPPORTED, Boolean.TRUE) - q.add_param(keys.PLAYLIST_INCLUDE_FRAMERATE, Boolean.TRUE) - q.add_param(keys.RTQOS, keys.CONTROL) - q.add_param(keys.PLAYER_BACKEND, keys.MEDIAPLAYER) - url = '?'.join([q.url, urlencode(q.params)]) - request_dict = { - 'url': url, - 'headers': q.headers - } - log.debug('live_request: |{0}|'.format(str(request_dict))) - return request_dict - - -@query -def _live(channel, token, headers={}): - signature = token[keys.SIGNATURE] - access_token = token[keys.VALUE] - - q = UsherQuery('api/channel/hls/{channel}.m3u8', headers=headers) - q.add_urlkw(keys.CHANNEL, channel) - q.add_param(keys.SIG, signature.encode('utf-8')) - q.add_param(keys.TOKEN, access_token.encode('utf-8')) - q.add_param(keys.ALLOW_SOURCE, Boolean.TRUE) - q.add_param(keys.ALLOW_SPECTRE, Boolean.TRUE) - q.add_param(keys.ALLOW_AUDIO_ONLY, Boolean.TRUE) - q.add_param(keys.FAST_BREAD, Boolean.TRUE) - q.add_param(keys.CDM, keys.WV) - q.add_param(keys.REASSIGNMENT_SUPPORTED, Boolean.TRUE) - q.add_param(keys.PLAYLIST_INCLUDE_FRAMERATE, Boolean.TRUE) - q.add_param(keys.RTQOS, keys.CONTROL) - q.add_param(keys.PLAYER_BACKEND, keys.MEDIAPLAYER) - return q - - -@m3u8 -def live(channel, platform=keys.WEB, headers={}): - token = channel_token(channel, platform=platform, headers=headers) - token = get_access_token(token) - if not token: - return ACCESS_TOKEN_EXCEPTION - elif isinstance(token, dict) and 'error' in token: - return token - else: - return _live(channel, token, headers=headers) - - -def video_request(video_id, platform=keys.WEB, headers={}): - video_id = valid_video_id(video_id) - if video_id: - token = vod_token(video_id, platform=platform, headers=headers) - token = get_access_token(token) - - if not token: - return ACCESS_TOKEN_EXCEPTION - elif isinstance(token, dict) and 'error' in token: - return token - else: - signature = token[keys.SIGNATURE] - access_token = token[keys.VALUE] - q = UsherQuery('vod/{id}', headers=headers) - q.add_urlkw(keys.ID, video_id) - q.add_param(keys.NAUTHSIG, signature.encode('utf-8')) - q.add_param(keys.NAUTH, access_token.encode('utf-8')) - q.add_param(keys.ALLOW_SOURCE, Boolean.TRUE) - q.add_param(keys.ALLOW_AUDIO_ONLY, Boolean.TRUE) - q.add_param(keys.CDM, keys.WV) - q.add_param(keys.REASSIGNMENT_SUPPORTED, Boolean.TRUE) - q.add_param(keys.PLAYLIST_INCLUDE_FRAMERATE, Boolean.TRUE) - q.add_param(keys.RTQOS, keys.CONTROL) - q.add_param(keys.PLAYER_BACKEND, keys.MEDIAPLAYER) - q.add_param(keys.BAKING_BREAD, Boolean.TRUE) - q.add_param(keys.BAKING_BROWNIES, Boolean.TRUE) - q.add_param(keys.BAKING_BROWNIES_TIMEOUT, 1050) - url = '?'.join([q.url, urlencode(q.params)]) - request_dict = { - 'url': url, - 'headers': q.headers - } - log.debug('video_request: |{0}|'.format(str(request_dict))) - return request_dict - else: - raise NotImplementedError('Unknown Video Type') - - -@query -def _vod(video_id, token, headers={}): - signature = token[keys.SIGNATURE] - access_token = token[keys.VALUE] - - q = UsherQuery('vod/{id}', headers=headers) - q.add_urlkw(keys.ID, video_id) - q.add_param(keys.NAUTHSIG, signature.encode('utf-8')) - q.add_param(keys.NAUTH, access_token.encode('utf-8')) - q.add_param(keys.ALLOW_SOURCE, Boolean.TRUE) - q.add_param(keys.ALLOW_AUDIO_ONLY, Boolean.TRUE) - q.add_param(keys.CDM, keys.WV) - q.add_param(keys.REASSIGNMENT_SUPPORTED, Boolean.TRUE) - q.add_param(keys.PLAYLIST_INCLUDE_FRAMERATE, Boolean.TRUE) - q.add_param(keys.RTQOS, keys.CONTROL) - q.add_param(keys.PLAYER_BACKEND, keys.MEDIAPLAYER) - q.add_param(keys.BAKING_BREAD, Boolean.TRUE) - q.add_param(keys.BAKING_BROWNIES, Boolean.TRUE) - q.add_param(keys.BAKING_BROWNIES_TIMEOUT, 1050) - return q - - -@m3u8 -def video(video_id, platform=keys.WEB, headers={}): - video_id = valid_video_id(video_id) - if video_id: - token = vod_token(video_id, platform=platform, headers=headers) - token = get_access_token(token) - - if not token: - return ACCESS_TOKEN_EXCEPTION - elif isinstance(token, dict) and 'error' in token: - return token - else: - return _vod(video_id, token, headers=headers) - else: - raise NotImplementedError('Unknown Video Type') - - -@clip_embed -@query -def clip(slug, headers={}): - qry = { - "operationName": "VideoAccessToken_Clip", - "extensions": { - "persistedQuery": { - "version": 1, - "sha256Hash": "36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11" - } - }, - "variables": { - "slug": slug - } - } - - q = ClipsQuery(headers=headers, data=json.dumps(qry)) - return q diff --git a/script.module.python.twitch/resources/lib/twitch/exceptions.py b/script.module.python.twitch/resources/lib/twitch/exceptions.py deleted file mode 100644 index a764f1a0b..000000000 --- a/script.module.python.twitch/resources/lib/twitch/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - - -class ResourceUnavailableException(Exception): - pass diff --git a/script.module.python.twitch/resources/lib/twitch/keys.py b/script.module.python.twitch/resources/lib/twitch/keys.py deleted file mode 100644 index c33d0e3a1..000000000 --- a/script.module.python.twitch/resources/lib/twitch/keys.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - string constants - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2019 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -AFTER = 'after' -ALLOW_AUDIO_ONLY = 'allow_audio_only' -ALLOW_SOURCE = 'allow_source' -ALLOW_SPECTRE = 'allow_spectre' -AVATAR_IMAGE = 'avatar_image' -BAKING_BREAD = 'baking_bread' -BAKING_BROWNIES = 'baking_brownies' -BAKING_BROWNIES_TIMEOUT = 'baking_brownies_timeout' -BEFORE = 'before' -BROADCAST_TYPE = 'broadcast_type' -BROADCASTER_ID = 'broadcaster_id' -BROADCASTER_LANGUAGE = 'broadcaster_language' -BROADCASTS = 'broadcasts' -CDM = 'cdm' -CHANNEL = 'channel' -CHANNEL_FEED_ENABLED = 'channel_feed_enabled' -CHANNEL_ID = 'channel_id' -CLIP = 'clip' -CODE = 'code' -COLLECTION_ID = 'collection_id' -COMMENT_ID = 'comment_id' -COMMENTS = 'comments' -COMMUNITIES = 'communities' -COMMUNITY_ID = 'community_id' -COMMUNITY_IDS = 'community_ids' -CONTAINING_ITEM = 'containing_item' -CONTENT = 'content' -CONTROL = 'control' -COUNT = 'count' -COVER_IMAGE = 'cover_image' -CURSOR = 'cursor' -CLIENT_ID = 'client_id' -DATA = 'data' -DELAY = 'delay' -DESCRIPTION = 'description' -DIRECTION = 'direction' -DURATION = 'duration' -EMAIL = 'email' -EMOTE_ID = 'emote_id' -EMOTESETS = 'emotesets' -ENDED_AT = 'ended_at' -ERROR = 'error' -EXTENSION_ID = 'extension_id' -FAST_BREAD = 'fast_bread' -FEATURED = 'featured' -FIRST = 'first' -FOLLOWS = 'follows' -FROM_ID = 'from_id' -GAME = 'game' -GAME_ID = 'game_id' -HAS_DELAY = 'has_delay' -HLS = 'hls' -ID = 'id' -IDENTIFIER = 'identifier' -INCLUDE_ALL_ITEMS = 'include_all_items' -ITEM_ID = 'item_id' -LANGUAGE = 'language' -LIMIT = 'limit' -LIVE = 'live' -LIVE_ONLY = 'live_only' -LOGIN = 'login' -MANIFEST_ID = 'manifest_id' -MEDIAPLAYER = 'mediaplayer' -MESSAGE = 'message' -NAME = 'name' -NAUTH = 'nauth' -NAUTHSIG = 'nauthsig' -NEED_HTTPS = 'need_https' -NOTIFICATIONS = 'notifications' -OFFSET = 'offset' -PART = 'part' -PERIOD = 'period' -PLATFORM = 'platform' -PLAYER_BACKEND = 'player_backend' -PLAYLIST_INCLUDE_FRAMERATE = 'playlist_include_framerate' -POSITION = 'position' -POST_ID = 'post_id' -QUERY = 'query' -REASON = 'reason' -REASSIGNMENT_SUPPORTED = 'reassignment_supported' -RTQOS = 'rtqos' -RULES = 'rules' -SCE_PLATFORM = 'sce_platform' -SHARE = 'share' -SIG = 'sig' -SIGNATURE = 'signature' -SLUG = 'slug' -SORT = 'sort' -SORT_BY = 'sortby' -STARTED_AT = 'started_at' -STATUS = 'status' -STREAM_PLAYBACK_ACCESS_TOKEN = 'streamPlaybackAccessToken' -STREAM_TYPE = 'stream_type' -SUMMARY = 'summary' -TAG_ID = 'tag_id' -TAG_IDS = 'tag_ids' -TAG_LIST = 'tag_list' -TARGET_ID = 'target_id' -TEAM = 'team' -TITLE = 'title' -TO_ID = 'to_id' -TOKEN = 'token' -TRENDING = 'trending' -TYPE = 'type' -USER = 'user' -USERNAME = 'username' -USER_AGENT = 'User-Agent' -USER_AGENT_STRING = ('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) ' - 'Gecko/20100101 Firefox/6.0') -USER_ID = 'user_id' -USER_LOGIN = 'user_login' -UPLOAD_TOKEN = 'upload_token' -VALUE = 'value' -VIDEO_ID = 'video_id' -VIDEO_PLAYBACK_ACCESS_TOKEN = 'videoPlaybackAccessToken' -VOD = 'vod' -WEB = 'web' -WV = 'wv' -XBOX_HEARTBEAT = 'xbox_heartbeat' diff --git a/script.module.python.twitch/resources/lib/twitch/log.py b/script.module.python.twitch/resources/lib/twitch/log.py deleted file mode 100644 index cc50925c1..000000000 --- a/script.module.python.twitch/resources/lib/twitch/log.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -import re -import logging -import copy - -try: - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - -try: - import xbmc -except ImportError: - xbmc = None - - -def _mask(message): - mask = '*' * 11 - masked_message = re.sub(r'((?:OAuth|Bearer)\s)[^\'"]+', r'\1' + mask, message) - masked_message = re.sub(r'(["\']email["\']:\s*[\'"])[^\'"]+', r'\1' + mask, masked_message) - masked_message = re.sub(r'(USER-IP=[\'"])[^\'"]+', r'\1' + mask, masked_message) - masked_message = re.sub(r'(["\']client_secret["\']:\s*[\'"])[^\'"]+', r'\1' + mask, masked_message) - masked_message = re.sub(r'(client_secret=).+?(&|$|\|)', r'\1' + mask + r'\2', masked_message) - masked_message = re.sub(r'(\\*["\']user_ip\\*["\']:\\*["\']).+?(\\*["\'])', r'\1' + mask + r'\2', masked_message) - masked_message = re.sub(r'(["\'](?:nauth)*sig["\']: ["\'])[^\'"]+', r'\1' + mask, masked_message) - return masked_message - - -def _add_leader(message): - if xbmc: - message = 'script.module.python.twitch: %s' % message - return message - - -def prep_log_message(message): - message = copy.deepcopy(message) - message = _mask(message) - message = _add_leader(message) - return message - - -class Log: - def __init__(self): - if xbmc: - self._log = xbmc.log - else: - self._log = logging.getLogger('twitch') - self._log.addHandler(NullHandler()) - - def info(self, message): - message = prep_log_message(message) - if xbmc: - self._log(message, xbmc.LOGINFO) - else: - self._log.info(message) - - def debug(self, message): - message = prep_log_message(message) - if xbmc: - self._log(message, xbmc.LOGDEBUG) - else: - self._log.debug(message) - - def warning(self, message): - message = prep_log_message(message) - if xbmc: - self._log(message, xbmc.LOGWARNING) - else: - self._log.debug(message) - - def error(self, message): - message = prep_log_message(message) - if xbmc: - self._log(message, xbmc.LOGERROR) - else: - self._log.error(message) - - def critical(self, message): - message = prep_log_message(message) - if xbmc: - self._log(message, xbmc.LOGFATAL) - else: - self._log.critical(message) - - def deprecated_query(self, old, new=None): - if new: - self.warning('DEPRECATED call to |{0}| detected, please use |{1}| instead'.format(old, new)) - else: - self.warning('DEPRECATED call to |{0}| detected, no alternatives available'.format(old)) - - def deprecated_endpoint(self, old): - self.warning('DEPRECATED call to |{0}| endpoint detected'.format(old)) - - def deprecated_api_version(self, old, new, eol_date): - self.warning('API version |{0}| is deprecated, update to |{1}| by |{2}|'.format(old, new, eol_date)) - - -log = Log() diff --git a/script.module.python.twitch/resources/lib/twitch/methods.py b/script.module.python.twitch/resources/lib/twitch/methods.py deleted file mode 100644 index 574ff007d..000000000 --- a/script.module.python.twitch/resources/lib/twitch/methods.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -GET = 'GET' -POST = 'POST' -PUT = 'PUT' -DELETE = 'DELETE' - -valid = [GET, POST, PUT, DELETE] - - -def validate(value): - if value in valid: - return value - raise ValueError(value) diff --git a/script.module.python.twitch/resources/lib/twitch/oauth/__init__.py b/script.module.python.twitch/resources/lib/twitch/oauth/__init__.py deleted file mode 100644 index c19f3763a..000000000 --- a/script.module.python.twitch/resources/lib/twitch/oauth/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -__all__ = ['default', 'helix', 'clients', 'validation'] - -from . import helix -from . import helix as default -from . import clients -from . import validation diff --git a/script.module.python.twitch/resources/lib/twitch/oauth/clients.py b/script.module.python.twitch/resources/lib/twitch/oauth/clients.py deleted file mode 100644 index 0b73f1344..000000000 --- a/script.module.python.twitch/resources/lib/twitch/oauth/clients.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from urllib.parse import urlsplit -from urllib.parse import urlencode - -from .. import CLIENT_ID, CLIENT_SECRET, methods -from ..queries import OAuthQuery as Qry -from ..queries import query - -class MobileClient: - def __init__(self, client_id='', client_secret=''): - self.client_id = client_id if client_id else CLIENT_ID - self.client_secret = client_secret if client_secret else CLIENT_SECRET - - def prepare_request_uri(self, redirect_uri='http://localhost:3000/', scope=list(), force_verify=False, state=''): - q = Qry('authorize') - q.add_param('response_type', 'token') - q.add_param('client_id', self.client_id) - q.add_param('redirect_uri', redirect_uri) - q.add_param('scope', ' '.join(scope)) - q.add_param('force_verify', str(force_verify).lower()) - q.add_param('state', state) - return '?'.join([q.url, urlencode(q.params)]) - - def prepare_token_uri(self, scope=list()): - q = Qry('token') - q.add_param('client_id', self.client_id) - q.add_param('client_secret', self.client_secret) - q.add_param('grant_type', 'client_credentials') - q.add_param('scope', ' '.join(scope)) - return '?'.join([q.url, urlencode(q.params)]) - - def prepare_revoke_uri(self, token): - q = Qry('revoke') - q.add_param('client_id', self.client_id) - q.add_param('token', token) - return '?'.join([q.url, urlencode(q.params)]) - - @query - def revoke_token(self, token): - q = Qry('revoke', method=methods.POST) - q.add_param('client_id', self.client_id) - q.add_param('token', token) - return q - - @query - def get_app_access_token(self, scope=list()): - q = Qry('token', method=methods.POST) - q.add_param('client_id', self.client_id) - q.add_param('client_secret', self.client_secret) - q.add_param('grant_type', 'client_credentials') - q.add_param('scope', ' '.join(scope)) - return q - - @staticmethod - def parse_implicit_response(url): - pairs = urlsplit(url).fragment.split('&') - fragment = dict() - for pair in pairs: - key, value = pair.split('=') - fragment[key] = value - return {'access_token': fragment.get('access_token'), 'scope': fragment.get('scope', '').split('+'), 'state': fragment.get('state')} diff --git a/script.module.python.twitch/resources/lib/twitch/oauth/helix/__init__.py b/script.module.python.twitch/resources/lib/twitch/oauth/helix/__init__.py deleted file mode 100644 index 652caae9a..000000000 --- a/script.module.python.twitch/resources/lib/twitch/oauth/helix/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -__all__ = ['scopes'] - -from . import scopes diff --git a/script.module.python.twitch/resources/lib/twitch/oauth/helix/scopes.py b/script.module.python.twitch/resources/lib/twitch/oauth/helix/scopes.py deleted file mode 100644 index d388d316e..000000000 --- a/script.module.python.twitch/resources/lib/twitch/oauth/helix/scopes.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - Reference: https://dev.twitch.tv/docs/authentication#scopes - - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -analytics_read_extensions = 'analytics:read:extensions' # View analytics data for the Twitch Extensions owned by the authenticated account. -analytics_read_games = 'analytics:read:games' # View analytics data for the games owned by the authenticated account. -bits_read = 'bits:read' # View Bits information for a channel. -channel_edit_commercial = 'channel:edit:commercial' # Run commercials on a channel. -channel_manage_broadcast = 'channel:manage:broadcast' # Manage a channel’s broadcast configuration, including updating channel configuration and managing stream markers and stream tags. -channel_manage_extensions = 'channel:manage:extensions' # Manage a channel’s Extension configuration, including activating Extensions. -channel_manage_polls = 'channel:manage:polls' # Manage a channel’s polls. -channel_manage_predictions = 'channel:manage:predictions' # Manage of channel’s Channel Points Predictions -channel_manage_redemptions = 'channel:manage:redemptions' # Manage Channel Points custom rewards and their redemptions on a channel. -channel_manage_schedule = 'channel:manage:schedule' # Manage a channel’s stream schedule. -channel_manage_videos = 'channel:manage:videos' # Manage a channel’s videos, including deleting videos. -channel_read_editors = 'channel:read:editors' # View a list of users with the editor role for a channel. -channel_read_goals = 'channel:read:goals' # View Creator Goals for a channel. -channel_read_hype_train = 'channel:read:hype_train' # View Hype Train information for a channel. -channel_read_polls = 'channel:read:polls' # View a channel’s polls. -channel_read_predictions = 'channel:read:predictions' # View a channel’s Channel Points Predictions. -channel_read_redemptions = 'channel:read:redemptions' # View Channel Points custom rewards and their redemptions on a channel. -channel_read_stream_key = 'channel:read:stream_key' # View an authorized user’s stream key. -channel_read_subscriptions = 'channel:read:subscriptions' # View a list of all subscribers to a channel and check if a user is subscribed to a channel. -clips_edit = 'clips:edit' # Manage Clips for a channel. -moderation_read = 'moderation:read' # View a channel’s moderation data including Moderators, Bans, Timeouts, and Automod settings. -moderator_manage_banned_users = 'moderator:manage:banned_users' # Ban and unban users. -moderator_read_blocked_terms = 'moderator:read:blocked_terms' # View a broadcaster’s list of blocked terms. -moderator_manage_blocked_terms = 'moderator:manage:blocked_terms' # Manage a broadcaster’s list of blocked terms. -moderator_manage_automod = 'moderator:manage:automod' # Manage messages held for review by AutoMod in channels where you are a moderator. -moderator_read_automod_settings = 'moderator:read:automod_settings' # View a broadcaster’s AutoMod settings. -moderator_manage_automod_settings = 'moderator:manage:automod_settings' # Manage a broadcaster’s AutoMod settings. -moderator_read_chat_settings = 'moderator:read:chat_settings' # View a broadcaster’s chat room settings. -moderator_manage_chat_settings = 'moderator:manage:chat_settings' # Manage a broadcaster’s chat room settings. -user_edit = 'user:edit' # Manage a user object. -user_edit_follows = 'user:edit:follows' # Deprecated. Was previously used for “Create User Follows” and “Delete User Follows.” See Deprecation of Create and Delete Follows API Endpoints. -user_manage_blocked_users = 'user:manage:blocked_users' # Manage the block list of a user. -user_read_blocked_users = 'user:read:blocked_users' # View the block list of a user. -user_read_broadcast = 'user:read:broadcast' # View a user’s broadcasting configuration, including Extension configurations. -user_read_email = 'user:read:email' # View a user’s email address. -user_read_follows = 'user:read:follows' # View the list of channels a user follows. -user_read_subscriptions = 'user:read:subscriptions' # View if an authorized user is subscribed to specific channels. -channel_moderate = 'channel:moderate' # Perform moderation actions in a channel. The user requesting the scope must be a moderator in the channel. -chat_edit = 'chat:edit' # Send live stream chat and rooms messages. -chat_read = 'chat:read' # View live stream chat and rooms messages. -whispers_read = 'whispers:read' # View your whisper messages. -whispers_edit = 'whispers:edit' # Send whisper messages. diff --git a/script.module.python.twitch/resources/lib/twitch/oauth/validation.py b/script.module.python.twitch/resources/lib/twitch/oauth/validation.py deleted file mode 100644 index 45b8be3a2..000000000 --- a/script.module.python.twitch/resources/lib/twitch/oauth/validation.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2022 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from ..queries import OAuthValidationQuery as Qry -from ..queries import query - - -@query -def validate(token=None): - q = Qry(token) - return q diff --git a/script.module.python.twitch/resources/lib/twitch/parser.py b/script.module.python.twitch/resources/lib/twitch/parser.py deleted file mode 100644 index b41345c82..000000000 --- a/script.module.python.twitch/resources/lib/twitch/parser.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -import re -from urllib.parse import urlencode - -from . import keys -from .log import log - -_m3u_pattern = re.compile( - r'#EXT-X-MEDIA:TYPE=VIDEO.*' - r'GROUP-ID="(?P[^"]*)",' - r'NAME="(?P[^"]*)"[,=\w]*\n' - r'#EXT-X-STREAM-INF:.*' - r'BANDWIDTH=(?P[0-9]+),' - r'(?:.*RESOLUTION="*(?P[0-9xX]+)"*,)?' - r'(?:.*FRAME-RATE=(?P[0-9.]+))?.*\n' - r'(?Phttp.*)') - -_error_pattern = re.compile(r'.*error(?P.+?).*', re.IGNORECASE) - - -def _find_frame_rate(group_id, group_name): - group_id = group_id.lower() - - if group_id == 'audio_only': - return None - elif group_id == 'chunked': - group_id = group_name.lower().replace('(source)', '').strip() - - info = group_id.split('p') - if len(info) > 1 and info[1]: - fps = float(info[1]) - else: - fps = 30.0 - - return fps - - -def m3u8(f): - def m3u8_wrapper(*args, **kwargs): - results = f(*args, **kwargs) - try: - results = results.decode('utf-8') - except AttributeError: - pass - if keys.ERROR in results: - if isinstance(results, dict): - return results - else: - error = re.search(_error_pattern, results) - if error: - return { - 'error': 'Error', - 'message': error.group('message'), - 'status': 404 - } - return m3u8_to_list(results) - - return m3u8_wrapper - - -def clip_embed(f): - def clip_embed_wrapper(*args, **kwargs): - return clip_embed_to_list(f(*args, **kwargs)) - - return clip_embed_wrapper - - -def m3u8_to_dict(string): - log.debug('m3u8_to_dict called for:\n{0}'.format(string)) - d = dict() - matches = re.finditer(_m3u_pattern, string) - for m in matches: - if m.group('group_name') == 'audio_only': - name = 'Audio Only' - elif m.group('group_id') == 'chunked': - name = 'Source' - else: - name = m.group('group_name') - - if m.group('fps'): - fps = float(m.group('fps')) - else: - fps = _find_frame_rate(m.group('group_id'), m.group('group_name')) - - d[m.group('group_id')] = { - 'id': m.group('group_id'), - 'name': name, - 'url': m.group('url'), - 'bandwidth': int(m.group('bandwidth')), - 'fps': fps, - 'resolution': m.group('resolution') - } - log.debug('m3u8_to_dict result:\n{0}'.format(d)) - return d - - -def m3u8_to_list(string): - log.debug('m3u8_to_list called for:\n{0}'.format(string)) - l = list() - matches = re.finditer(_m3u_pattern, string) - for m in matches: - if m.group('group_name') == 'audio_only': - name = 'Audio Only' - elif m.group('group_id') == 'chunked': - name = 'Source' - else: - name = m.group('group_name') - - if m.group('fps'): - fps = float(m.group('fps')) - else: - fps = _find_frame_rate(m.group('group_id'), m.group('group_name')) - - l.append({ - 'id': m.group('group_id'), - 'name': name, - 'url': m.group('url'), - 'bandwidth': int(m.group('bandwidth')), - 'fps': fps, - 'resolution': m.group('resolution') - }) - - log.debug('m3u8_to_list result:\n{0}'.format(l)) - return l - - -def clip_embed_to_list(response): - log.debug('clip_embed_to_list called for:\n{0}'.format(response)) - - clip_json = response.get('data', {}).get('clip', {}) - access_token = clip_json.get('playbackAccessToken', {}) - token = access_token.get('value', '') - signature = access_token.get('signature', '') - qualities = clip_json.get('videoQualities', []) - - params = urlencode({ - 'sig': signature, - 'token': token - }) - - l = list() - - if isinstance(response, dict): - clip = response.get('data', {}).get('clip', {}) - qualities = clip.get('videoQualities', list()) - - if qualities: - l = [{ - 'id': item['quality'], - 'name': item['quality'], - 'url': item['sourceURL'] + '?' + params, - 'bandwidth': -1 - } for item in qualities] - if l: - l.insert(0, { - 'id': 'Source', - 'name': 'Source', - 'url': l[0]['url'], - 'bandwidth': -1 - }) - - log.debug('clip_embed_to_list result:\n{0}'.format(l)) - return l diff --git a/script.module.python.twitch/resources/lib/twitch/queries.py b/script.module.python.twitch/resources/lib/twitch/queries.py deleted file mode 100644 index 63c299e4d..000000000 --- a/script.module.python.twitch/resources/lib/twitch/queries.py +++ /dev/null @@ -1,262 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -from copy import deepcopy -from urllib.parse import urljoin - -from . import CLIENT_ID, OAUTH_TOKEN, APP_TOKEN -from .exceptions import ResourceUnavailableException -from .log import log, prep_log_message -from .scraper import download, get_json, get_json_and_headers -from . import methods - -_kraken_baseurl = 'https://api.twitch.tv/kraken/' -_helix_baseurl = 'https://api.twitch.tv/helix/' -_hidden_baseurl = 'https://api.twitch.tv/api/' -_usher_baseurl = 'https://usher.ttvnw.net/' -_clips_baseurl = 'https://gql.twitch.tv/gql' -_uploads_baseurl = 'https://uploads.twitch.tv/' -_gql_baseurl = 'https://gql.twitch.tv/gql' -_oauth_baseurl = 'https://api.twitch.tv/kraken/oauth2/' -_oauthid_baseurl = 'https://id.twitch.tv/oauth2/' - - -class _Query(object): - def __init__(self, url, headers={}, data={}, method=methods.GET): - self._headers = headers - self._data = data - self._url = url - self._method = method - - self._params = dict() - self._urlkws = dict() - - @property - def url(self): - formatted_url = self._url.format(**self._urlkws) # throws KeyError - return formatted_url - - @property - def headers(self): - return self._headers - - @property - def data(self): - return self._data - - @property - def params(self): - return self._params - - @property - def method(self): - return self._method - - @property - def urlkws(self): - return self._urlkws - - def add_path(self, path): - self._url = urljoin(self._url, path) - return self - - def add_data(self, key, value, default=None): - assert_new(self._data, key) - if value != default: - self._data[key] = value - return self - - def add_bin(self, data): - self._data = data - return self - - def add_param(self, key, value, default=None): - assert_new(self._params, key) - if value != default: - self._params[key] = value - return self - - def add_urlkw(self, kw, replacement): - assert_new(self._urlkws, kw) - self._urlkws[kw] = replacement - return self - - def set_headers(self, headers): - self._headers = headers - return self - - def __str__(self): - return '{method} Query to {url}, params {params}, data {data}, headers {headers}' \ - .format(url=self.url, params=self.params, headers=self.headers, data=self.data, method=self.method) - - def execute(self, f): - try: - return f(self.url, self.params, self.headers, self.data, self.method) - except: - raise ResourceUnavailableException(prep_log_message(str(self))) - - -class DownloadQuery(_Query): - def execute(self): - # TODO implement download completely here - return super(DownloadQuery, self).execute(download) - - -class JsonQuery(_Query): - def execute(self): - # TODO implement get_json completely here - return super(JsonQuery, self).execute(get_json) - - -class HelixJsonQuery(_Query): - def execute(self): - # TODO implement get_json completely here - return super(HelixJsonQuery, self).execute(get_json_and_headers) - - -class ApiQuery(JsonQuery): - def __init__(self, path, headers={}, data={}, use_token=True, method=methods.GET): - _headers = deepcopy(headers) - _headers.setdefault('Client-ID', CLIENT_ID) - if use_token and OAUTH_TOKEN: - _headers.setdefault('Authorization', 'OAuth {access_token}'.format(access_token=OAUTH_TOKEN)) - super(ApiQuery, self).__init__(_kraken_baseurl, _headers, data, method) - self.add_path(path) - - -class HelixApiQuery(HelixJsonQuery): - def __init__(self, path, headers={}, data={}, use_app_token=False, method=methods.GET): - _headers = deepcopy(headers) - _headers.setdefault('Client-ID', CLIENT_ID) - if use_app_token and APP_TOKEN: - _headers.setdefault('Authorization', 'Bearer {access_token}'.format(access_token=APP_TOKEN)) - elif OAUTH_TOKEN: - _headers.setdefault('Authorization', 'Bearer {access_token}'.format(access_token=OAUTH_TOKEN)) - super(HelixApiQuery, self).__init__(_helix_baseurl, _headers, data, method) - self._params = list() - self.add_path(path) - - def add_param(self, key, value, default=None): - if value != default: - if isinstance(value, list): - _params = [] - for val in value: - _params += [(key, val)] - self._params += _params - elif (key, value) not in self._params: - self._params += [(key, value)] - return self - - -class HiddenApiQuery(JsonQuery): - def __init__(self, path, headers={}, data={}, use_token=True, method=methods.GET): - _headers = deepcopy(headers) - if 'Client-ID' not in _headers: - _headers.setdefault('Client-ID', CLIENT_ID) - if 'Client-ID' in _headers and not _headers.get('Client-ID'): - del _headers['Client-ID'] - if 'Authorization' not in _headers: - if use_token and OAUTH_TOKEN: - _headers.setdefault('Authorization', 'OAuth {access_token}'.format(access_token=OAUTH_TOKEN)) - if 'Authorization' in _headers and not _headers.get('Authorization'): - del _headers['Authorization'] - super(HiddenApiQuery, self).__init__(_hidden_baseurl, _headers, data, method) - self.add_path(path) - - -class UsherQuery(DownloadQuery): - def __init__(self, path, headers={}, data={}, method=methods.GET): - _headers = deepcopy(headers) - if 'Client-ID' not in _headers: - _headers.setdefault('Client-ID', CLIENT_ID) - if 'Client-ID' in _headers and not _headers.get('Client-ID'): - del _headers['Client-ID'] - if 'Authorization' not in _headers: - if OAUTH_TOKEN: - _headers.setdefault('Authorization', 'OAuth {access_token}'.format(access_token=OAUTH_TOKEN)) - if 'Authorization' in _headers and not _headers.get('Authorization'): - del _headers['Authorization'] - super(UsherQuery, self).__init__(_usher_baseurl, _headers, data, method) - self.add_path(path) - - -class OAuthQuery(JsonQuery): - def __init__(self, path, headers={}, data={}, method=methods.GET): - _headers = deepcopy(headers) - super(JsonQuery, self).__init__(_oauthid_baseurl, _headers, data, method) - self.add_path(path) - - -class OAuthValidationQuery(JsonQuery): - def __init__(self, token=None): - _headers = {} - if token: - _headers['Authorization'] = 'OAuth {access_token}'.format(access_token=token) - if 'Authorization' not in _headers: - _headers.setdefault('Authorization', 'OAuth {access_token}'.format(access_token=OAUTH_TOKEN)) - super(JsonQuery, self).__init__(_oauthid_baseurl, _headers, {}, methods.GET) - self.add_path('validate') - - -class ClipsQuery(JsonQuery): - def __init__(self, path='', headers={}, data={}, method=methods.POST): - _headers = deepcopy(headers) - super(ClipsQuery, self).__init__(_clips_baseurl, _headers, data, method) - if path: - self.add_path(path) - - -class UploadsQuery(DownloadQuery): - def __init__(self, path, headers={}, data={}, method=methods.PUT): - _headers = deepcopy(headers) - super(UploadsQuery, self).__init__(_uploads_baseurl, _headers, data, method) - self.add_path(path) - - -class V5Query(ApiQuery): - def __init__(self, path, use_token=True, method=methods.GET): - headers = {'ACCEPT': 'application/vnd.twitchtv.v5+json'} - super(V5Query, self).__init__(path, headers, use_token=use_token, method=method) - - -class HelixQuery(HelixApiQuery): - def __init__(self, path, use_app_token=False, method=methods.GET): - super(HelixQuery, self).__init__(path, use_app_token=use_app_token, method=method) - - -class GQLQuery(JsonQuery): - def __init__(self, path, headers={}, data={}, use_token=True, method=methods.POST): - _headers = deepcopy(headers) - _headers.setdefault('Client-ID', CLIENT_ID) - if use_token and OAUTH_TOKEN: - _headers.setdefault('Authorization', 'OAuth {access_token}'.format(access_token=OAUTH_TOKEN)) - super(GQLQuery, self).__init__(_gql_baseurl, _headers, data, method) - self.add_path(path) - - -def assert_new(d, k): - if k in d: - v = d.get(k) - raise ValueError('Key |{0}| already set to |{1}|'.format(k, v)) - - -# TODO maybe rename -def query(f): - def wrapper(*args, **kwargs): - qry = f(*args, **kwargs) - if not isinstance(qry, _Query): - raise ValueError('|{0}| did not return a Query, was: |{1}|'.format(f.__name__, repr(qry))) - log.debug('{0} QUERY: url: |{1}|, params: |{2}|, data: |{3}|, headers: |{4}|, target_func: |{5}|' - .format(qry.method, qry.url, qry.params, qry.data, qry.headers, f.__name__)) - return qry.execute() - - return wrapper diff --git a/script.module.python.twitch/resources/lib/twitch/scraper.py b/script.module.python.twitch/resources/lib/twitch/scraper.py deleted file mode 100644 index 433d95db0..000000000 --- a/script.module.python.twitch/resources/lib/twitch/scraper.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- encoding: utf-8 -*- -""" - - Copyright (C) 2012-2016 python-twitch (https://github.com/ingwinlu/python-twitch) - Copyright (C) 2016-2018 script.module.python.twitch - - This file is part of script.module.python.twitch - - SPDX-License-Identifier: GPL-3.0-only - See LICENSES/GPL-3.0-only for more information. -""" - -import sys -from urllib.error import URLError -from urllib.parse import quote_plus -from urllib.parse import urlencode - -import requests - -from .keys import USER_AGENT, USER_AGENT_STRING -from .log import log -from .exceptions import ResourceUnavailableException -from . import methods - -try: - import json -except: - import simplejson as json # @UnresolvedImport - -SSL_VERIFICATION = True -if sys.version_info <= (2, 7, 9): - SSL_VERIFICATION = False - -MAX_RETRIES = 5 - - -def get_json(baseurl, parameters={}, headers={}, data={}, method=methods.GET): - '''Download Data from an URL and returns it as JSON - @param url Url to download from - @param parameters Parameter dict to be encoded with url or list of tuple pairs - @param headers Headers dict to pass with Request - @param data Request body - @param method Request method - @returns JSON Object with data from URL - ''' - method = methods.validate(method) - jsonString = download(baseurl, parameters, headers, data, method) - jsonDict = json.loads(jsonString) - log.debug('url: |{0}| parameters: |{1}|\n{2}'.format(baseurl, parameters, json.dumps(jsonDict, indent=4, sort_keys=True))) - return jsonDict - - -def get_json_and_headers(baseurl, parameters={}, headers={}, data={}, method=methods.GET): - '''Download Data from an URL and returns it as JSON - @param url Url to download from - @param parameters Parameter dict to be encoded with url or list of tuple pairs - @param headers Headers dict to pass with Request - @param data Request body - @param method Request method - @returns JSON Object with data and headers from URL {'response': {}, 'headers': {}} - ''' - method = methods.validate(method) - content = download(baseurl, parameters, headers, data, method, response_headers=True) - content['response'] = json.loads(content['response']) - log.debug('url: |{0}| parameters: |{1}|\n{2}'.format(baseurl, parameters, json.dumps(content['response'], indent=4, sort_keys=True))) - return content - - -def download(baseurl, parameters={}, headers={}, data={}, method=methods.GET, response_headers=False): - '''Download Data from an url and returns it as a String - @param method Request method - @param baseurl Url to download from (e.g. http://www.google.com) - @param parameters Parameter dict to be encoded with url or list of tuple pairs - @param headers Headers dict to pass with Request - @param data Request body - @param method Request method - @param response_headers Include response headers in response {'response': {}, 'headers': {}} - @returns String of data from URL or {'response': {}, 'headers': {}} if response_headers is True - ''' - method = methods.validate(method) - - if not parameters: - url = baseurl - elif isinstance(parameters, dict): - url = '?'.join([baseurl, urlencode(parameters)]) - else: - _parameters = '' - for param in parameters: - _parameters += '{0}={1}&'.format(param[0], quote_plus(str(param[1]))) - _parameters = _parameters.rstrip('&') - url = '?'.join([baseurl, _parameters]) - - log.debug('Downloading: |{0}|'.format(url)) - content = "" - for _ in range(MAX_RETRIES): - try: - headers.update({USER_AGENT: USER_AGENT_STRING}) - if isinstance(data, list): - json_body = data - data = None - else: - json_body = None - response = requests.request(method=method, url=url, headers=headers, - json=json_body, data=data, verify=SSL_VERIFICATION) - content = response.content - if not content: - content = '{{"status": {0}}}'.format(response.status_code) - break - except Exception as err: - if not isinstance(err, URLError): - log.debug('Error |{0}| during HTTP Request, abort'.format(repr(err))) - raise # propagate non-URLError - log.debug('Error |{0}| during HTTP Request, retrying'.format(repr(err))) - else: - raise ResourceUnavailableException('Max retries exceeded') - - if isinstance(content, bytes): - content = content.decode('utf-8') - - if not response_headers: - return content - else: - return {'response': content, 'headers': response.headers} diff --git a/script.module.python.twitch/resources/media/icon.png b/script.module.python.twitch/resources/media/icon.png deleted file mode 100644 index d51a68046..000000000 Binary files a/script.module.python.twitch/resources/media/icon.png and /dev/null differ diff --git a/script.module.t1mlib/LICENSE.txt b/script.module.t1mlib/LICENSE.txt deleted file mode 100644 index 4f8e8eb30..000000000 --- a/script.module.t1mlib/LICENSE.txt +++ /dev/null @@ -1,282 +0,0 @@ - - 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/script.module.t1mlib/addon.xml b/script.module.t1mlib/addon.xml deleted file mode 100644 index 59d539963..000000000 --- a/script.module.t1mlib/addon.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - t1m Library Routines - t1m Library Routines - Feel free to use this script. - GPL-2.0-or-later - all - https://forum.kodi.tv/showthread.php?tid=247163 - https://github.com/learningit/repo-scripts> - en - v4.0.8 - fixes for Nexus - - resources/icon.png - - - diff --git a/script.module.t1mlib/lib/t1mlib.py b/script.module.t1mlib/lib/t1mlib.py deleted file mode 100644 index 94a64ce3c..000000000 --- a/script.module.t1mlib/lib/t1mlib.py +++ /dev/null @@ -1,304 +0,0 @@ -# -*- coding: utf-8 -*- -# Framework Video Addon Routines for Kodi -# For Kodi Matrix (v19) and above -# -# -import sys -import os -import xbmc -import xbmcgui -import xbmcaddon -import xbmcplugin -import xbmcvfs -import urllib.parse -import calendar -import datetime -import requests -import string -import locale - - -qp = urllib.parse.quote_plus -uqp = urllib.parse.unquote_plus -USERAGENT = 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36' -httpHeaders = {'User-Agent': USERAGENT, - 'Accept':"application/json, text/javascript, text/html,*/*", - 'Accept-Encoding':'gzip,deflate,sdch', - 'Accept-Language':'en-US,en;q=0.8' - } - - -class t1mAddon(object): - - def __init__(self, aname): - self.script = xbmcaddon.Addon('script.module.t1mlib') - self.addon = xbmcaddon.Addon(''.join(['plugin.video.', aname])) - self.addonName = self.addon.getAddonInfo('name') - self.localLang = self.addon.getLocalizedString - self.homeDir = self.addon.getAddonInfo('path') - self.addonIcon = xbmcvfs.translatePath(os.path.join(self.homeDir, 'resources', 'icon.png')) - self.addonFanart = xbmcvfs.translatePath(os.path.join(self.homeDir,'resources' 'fanart.jpg')) - self.defaultHeaders = httpHeaders - self.defaultVidStream = {'codec': 'h264', 'width': 1280, 'height': 720, 'aspect': 1.78} - self.defaultAudStream = {'codec': 'aac', 'language': 'en'} - self.defaultSubStream = {'language': 'en'} - self.log(''.join(["Python version : ",str(sys.version)])) - self.log(''.join(["locale.getpreferredencoding : ",str(locale.getpreferredencoding())])) - - - def log(self, txt): - message = ''.join([self.addonName, ' : ', txt]) - xbmc.log(msg=message, level=xbmc.LOGDEBUG) - - - def addMenuItem(self, name, mode, ilist=None, url=None, thumb=None, fanart=None, - videoInfo=None, videoStream=None, audioStream=None, - subtitleStream=None, cm=None, isFolder=True): - videoStream = self.defaultVidStream - audioStream = self.defaultAudStream - subtitleStream = self.defaultSubStream - liz = xbmcgui.ListItem(name, offscreen=True) - liz.setArt({'thumb': thumb, 'fanart': fanart, 'poster':thumb}) - liz.setInfo('Video', videoInfo) - liz.addStreamInfo('video', videoStream) - liz.addStreamInfo('audio', audioStream) - liz.addStreamInfo('subtitle', subtitleStream) - if cm is not None: - liz.addContextMenuItems(cm) - if not isFolder: - liz.setProperty('IsPlayable', 'true') - u = ''.join([sys.argv[0], '?mode=', str(mode), '&url=']) - if url is not None: - u = ''.join([u, qp(url)]) - ilist.append((u, liz, isFolder)) - return ilist - - # override or extend these functions in the specific addon default.py - - def getAddonMenu(self, url, ilist): - return ilist - - def getAddonCats(self, url, ilist): - return ilist - - def getAddonMovies(self, url, ilist): - return ilist - - def getAddonShows(self, url, ilist): - return ilist - - def getAddonEpisodes(self, url, ilist): - return ilist - - def getAddonSearch(self, url, ilist): - return ilist - - - def getAddonListing(self, url, ilist): - url, sta, sids = url.split('|') - sid = sids.split('%',1)[0] - sid = int(sid) - d = datetime.datetime.utcnow() - now = calendar.timegm(d.utctimetuple()) -# fix some legacy stuff for existing add-ons - if sta == '20517': - sta = '9133006313' - if sid == 94072: - sid = 95652 - elif sta == '20534': - sta = '9133000248' - if sid == 35794: - sid = 93301 - elif sid == 39303: - sid = 14506 - elif sid ==17075: - sid = 87003 - a = requests.get(''.join(['https://cmg-prod.apigee.net/v1/xapi/tvschedules/tvguide/',sta,'/web?start=',str(now),'&duration=180']), headers=self.defaultHeaders).json() - for c in a["data"]["items"]: - if c["channel"]["legacySourceId"] == sid: - for b in c["programSchedules"]: - st = datetime.datetime.fromtimestamp(float(b['startTime'])).strftime('%H:%M') - et = datetime.datetime.fromtimestamp(float(b['endTime'])).strftime('%H:%M') - duration = int(float(b['endTime']) - float(b['startTime'])) - name = ''.join([st,' - ',et,' ',str(b.get('title'))]) - d = requests.get(b["programDetails"], headers=self.defaultHeaders).json()['data']['item'] - if d.get('type') == 'show' or d.get('type') == None: - name = b.get('title') - epiname = d.get('episodeTitle',name) - if epiname == None: - epiname = '' - infoList = {'mediatype':'episode', - 'Title': name, - 'duration': duration, - 'Plot': ''.join([st,' - ',et,' ',str(duration/60),' min.\n\n[COLOR blue]',str(name),'\n',str(epiname),'[/COLOR]\n\n',str(d.get('description',''))]), - 'MPAA': b.get('rating') - } - else: - name = d.get('title') - infoList = {'mediatype':'movie', - 'Title': name, - 'duration': duration, - 'Plot': ''.join([st,' - ',et,' ',str(duration/60),' min.\n\n[COLOR blue]',str(d.get('title')),'[/COLOR]\n\n',str(d.get('description'))]), - 'MPAA': b.get('rating') - } - - thumb = self.addonIcon - fanart = self.addonFanart - ilist = self.addMenuItem(name,'LV', ilist, url, thumb, fanart, infoList, isFolder=False) - break - return(ilist) - - - def getAddonLiveVideo(self, url): - liz = xbmcgui.ListItem(path = url, offscreen=True) - liz.setProperty('inputstream','inputstream.adaptive') - liz.setProperty('inputstream.adaptive.manifest_type','hls') - liz.setMimeType('application/x-mpegURL') - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, liz) - - - def getAddonVideo(self, url): - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, xbmcgui.ListItem(path=uqp(url), offscreen=True)) - - - def doFunction(self, url): - return - - - def cleanFilename(self, filename): - whitelist = "-_.() %s%s" % (string.ascii_letters, string.digits) - filename = ''.join(c for c in filename if c in whitelist) - return filename - - - def makeLibraryPath(self, ftype, name=None): - if name is None: - name = self.cleanFilename(xbmc.getInfoLabel('ListItem.Title').replace('(Series)','',1).strip()) - profile = self.script.getAddonInfo('profile') - moviesDir = xbmcvfs.translatePath(os.path.join(profile,str(ftype))) - movieDir = xbmcvfs.translatePath(os.path.join(moviesDir, name)) - if not os.path.isdir(movieDir): - os.makedirs(movieDir) - return movieDir - - def doScan(self,movieDir): - json_cmd = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan", "params": {"directory":"%s/"},"id":1}' % movieDir.replace('\\','/') - jsonRespond = xbmc.executeJSONRPC(json_cmd) - - - def addMusicVideoToLibrary(self, url): - from xml.etree.ElementTree import Element - from xml.etree.ElementTree import tostring - import html.parser - from xml.dom import minidom - UNESCAPE = html.parser.HTMLParser().unescape - - url, infoList = urllib.parse.unquote_plus(url).split('||',1) - infoList = eval(infoList) - artist = infoList.get('artist') - title = infoList.get('title') - movieDir = self.makeLibraryPath('music_videos', name=self.cleanFilename(artist)) - strmFile = xbmcvfs.translatePath(os.path.join(movieDir, ''.join([self.cleanFilename(title),'.strm']))) - url = ''.join([sys.argv[0],'?mode=GV&url=',url]) - with open(strmFile, 'w') as outfile: - outfile.write(url) - nfoFile = xbmcvfs.translatePath(os.path.join(movieDir, ''.join([self.cleanFilename(title),'.nfo']))) - nfoData = Element('musicvideo') - for key, val in infoList.items(): - child = Element(key) - child.text = str(val) - nfoData.append(child) - - nfoData = UNESCAPE(minidom.parseString(tostring(nfoData)).toprettyxml(indent=" ")) -# the next lines of code fail with a Type Error on Android if 'wb' and .encode('utf-8) aren't used. Works ok on Windows and Linux though - with open(nfoFile, 'wb') as outfile: - outfile.write(nfoData.encode('utf-8')) - self.doScan(movieDir) - - - def addMovieToLibrary(self, url): - name = self.cleanFilename(''.join([xbmc.getInfoLabel('ListItem.Title'),'.strm'])) - movieDir = self.makeLibraryPath('movies') - strmFile = xbmcvfs.translatePath(os.path.join(movieDir, name)) - url = ''.join([sys.argv[0],'?mode=GV&url=',url]) - with open(strmFile, 'w') as outfile: - outfile.write(url) - self.doScan(movieDir) - - - def addShowByDate(self,url): - url = uqp(url) - movieDir = self.makeLibraryPath('shows') - ilist = [] - ilist = self.getAddonEpisodes(url, ilist) - for url, liz, isFolder in ilist: - pdate = str(liz.getVideoInfoTag().getFirstAired()) - pdate = pdate.split('/') - pdate = ''.join([pdate[2],'-',pdate[0],'-',pdate[1]]) - title = self.cleanFilename(str(liz.getVideoInfoTag().getTitle())) - TVShowTitle = self.cleanFilename(str(liz.getVideoInfoTag().getTVShowTitle())) - se = ''.join([TVShowTitle,' ',pdate,' [',title,'].strm']) - strmFile = xbmcvfs.translatePath(os.path.join(movieDir, se)) - with open(strmFile, 'w') as outfile: - outfile.write(url) - self.doScan(movieDir) - - - def addShowToLibrary(self,url): - movieDir = self.makeLibraryPath('shows') - ilist = [] - ilist = self.getAddonEpisodes(url, ilist) - for url, liz, isFolder in ilist: - season = str(liz.getVideoInfoTag().getSeason()) - episode = str(liz.getVideoInfoTag().getEpisode()) - title = self.cleanFilename(str(liz.getVideoInfoTag().getTitle())) - se = ''.join(['S',season,'E',episode,' ',title,'.strm']) - strmFile = xbmcvfs.translatePath(os.path.join(movieDir, se)) - with open(strmFile, 'w') as outfile: - outfile.write(url) - self.doScan(movieDir) - - - # internal functions for views, cache and directory management - - def procDir(self, dirFunc, url, content, cache2Disc=True): - ih = int(sys.argv[1]) - xbmcplugin.setContent(ih, content) - xbmcplugin.addSortMethod(ih, xbmcplugin.SORT_METHOD_UNSORTED) - xbmcplugin.addSortMethod(ih, xbmcplugin.SORT_METHOD_TITLE) - xbmcplugin.addSortMethod(ih, xbmcplugin.SORT_METHOD_EPISODE) - ilist = dirFunc(url, []) - xbmcplugin.addDirectoryItems(ih, ilist, len(ilist)) - xbmcplugin.endOfDirectory(ih, cacheToDisc=cache2Disc) - - def getVideo(self, url, ilist): - self.getAddonVideo(url) - - - def processAddonEvent(self): - mtable = {None : [self.getAddonMenu, 'files'], - 'GC' : [self.getAddonCats, 'files'], - 'GM' : [self.getAddonMovies, 'movies'], - 'GS' : [self.getAddonShows, 'tvshows'], - 'GE' : [self.getAddonEpisodes, 'episodes'], - 'SE' : [self.getAddonSearch, 'movies'], - 'GL' : [self.getAddonListing, 'episodes']} - ftable = {'GV' : self.getAddonVideo, - 'LV' : self.getAddonLiveVideo, - 'AM' : self.addMovieToLibrary, - 'AS' : self.addShowToLibrary, - 'AD' : self.addShowByDate, - 'MU' : self.addMusicVideoToLibrary, - 'DF' : self.doFunction} - parms = {} - if len((sys.argv[2][1:])) > 0: - parms = dict(arg.split("=") for arg in ((sys.argv[2][1:]).split("&"))) - for key in parms: - parms[key] = uqp(parms[key]) - fun = mtable.get(parms.get('mode')) - if fun != None: - self.procDir(fun[0],parms.get('url'),fun[1]) - else: - fun = ftable.get(parms.get('mode')) - if fun != None: - fun(parms.get('url')) diff --git a/script.module.t1mlib/readme.txt b/script.module.t1mlib/readme.txt deleted file mode 100644 index 21a4b6471..000000000 --- a/script.module.t1mlib/readme.txt +++ /dev/null @@ -1,9 +0,0 @@ -script.module.t1mlib -================ - -Library of support routines for t1m addons - -V4.0.4 Matrix version - added music video support -V4.0.6 - fix for Android file write having a Type Error -v4.0.7 - fix tv listings for live stream addons -V4.0.9 - fixes for Nexus \ No newline at end of file diff --git a/script.module.t1mlib/resources/icon.png b/script.module.t1mlib/resources/icon.png deleted file mode 100644 index ca4c53adb..000000000 Binary files a/script.module.t1mlib/resources/icon.png and /dev/null differ diff --git a/script.module.tubed.api/LICENSES/GPL-2.0-only.txt b/script.module.tubed.api/LICENSES/GPL-2.0-only.txt deleted file mode 100644 index d7f105139..000000000 --- a/script.module.tubed.api/LICENSES/GPL-2.0-only.txt +++ /dev/null @@ -1,339 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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 - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/script.module.tubed.api/addon.xml b/script.module.tubed.api/addon.xml deleted file mode 100644 index 0e886b4e6..000000000 --- a/script.module.tubed.api/addon.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - -- removed support for Kodi Matrix v19 - - - resources/media/icon.png - resources/media/fanart.jpg - - all - GPL-2.0-only - https://github.com/anxdpanic/script.module.tubed.api - Doplněk pro přístup k YouTube Data API - Et modul for adgang til YouTubes data-API - Ein Modul für den Zugriff auf die YouTube-Daten-API - Μονάδα για προσπέλαση του API δεδομένων του Youtube - A module to access YouTube's Data API - A module to access YouTube's Data API - A module to access YouTube's Data API - Un módulo para accedee al API de Datos de YouTube - Moduuli YouTube Data -rajapinnan käyttöön - Modul za pristup YouTubovom API-ju - Un modulo per accedere ai dati API di YouTube - YouTube의 데이터 API에 액세스하기 위한 모듈 - Модуль для доступа к YouTube Data API - Modul na prístup k údajom API služby Youtube - 访问 YouTube 数据 API 的模块 - Doplněk API Tubed poskytuje pohodlný způsob přístupu k YouTube Data API pro Kodi 19+ - Tubed API-modulet giver en praktisk måde at få adgang til YouTubes data-API i Kodi 19+ - Das Tubed-API-Modul stellt eine komfortable Möglichkeit zum Zugriff auf die YouTube-Daten-API in Kodi 19+ zur Verfügung - Η μονάδα Tubed API παρέχει έναν βολικό τρόπο προσπέλασης του API δεδομένων του Youtube στο Kodi 19+ - The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+ - The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+ - El módulo API Tubed provee una manera conveniente de acceder al API de Datos de YouTube en Kodi 19+ - Tubed API-moduuli tarjoaa helpon tavan YouTube Data -rajapinnan käyttöön Kodi versioissa 19+ - Tubed API modul omogućuje pouzdan način pristupa YouTubovom API-ju u Kodi 19+ - Il modulo Tubed API fornisce un modo conveniente per accedere all'API dei dati di YouTube in Kodi 19+ - Tubed API 모듈은 Kodi 19+에서 YouTube의 Data API에 액세스하는 편리한 방법을 제공합니다 - API модуль Tubed предоставляет удобный доступ к YouTube Data API в Kodi 19+ - Tubed API 模块为 Kodi 19+ 提供了访问 YouTube 数据 API 的便捷方式 - - diff --git a/script.module.tubed.api/resources/language/resource.language.af_za/strings.po b/script.module.tubed.api/resources/language/resource.language.af_za/strings.po deleted file mode 100644 index 085fc3690..000000000 --- a/script.module.tubed.api/resources/language/resource.language.af_za/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Afrikaans (South Africa) \n" -"Language: af_za\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Ontfout" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.am_et/strings.po b/script.module.tubed.api/resources/language/resource.language.am_et/strings.po deleted file mode 100644 index 51c0140db..000000000 --- a/script.module.tubed.api/resources/language/resource.language.am_et/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: am_et\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ar_sa/strings.po b/script.module.tubed.api/resources/language/resource.language.ar_sa/strings.po deleted file mode 100644 index cba10017b..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ar_sa/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ar_sa\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ast_es/strings.po b/script.module.tubed.api/resources/language/resource.language.ast_es/strings.po deleted file mode 100644 index e509f3b43..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ast_es/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Asturian (Spain) \n" -"Language: ast_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depuración" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.az_az/strings.po b/script.module.tubed.api/resources/language/resource.language.az_az/strings.po deleted file mode 100644 index 2fdd62e90..000000000 --- a/script.module.tubed.api/resources/language/resource.language.az_az/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: az_az\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.be_by/strings.po b/script.module.tubed.api/resources/language/resource.language.be_by/strings.po deleted file mode 100644 index 439d2985d..000000000 --- a/script.module.tubed.api/resources/language/resource.language.be_by/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Belarusian \n" -"Language: be_by\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Адсочваць хібы" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.bg_bg/strings.po b/script.module.tubed.api/resources/language/resource.language.bg_bg/strings.po deleted file mode 100644 index 1ff10060b..000000000 --- a/script.module.tubed.api/resources/language/resource.language.bg_bg/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Bulgarian \n" -"Language: bg_bg\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Дебъгване" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.bs_ba/strings.po b/script.module.tubed.api/resources/language/resource.language.bs_ba/strings.po deleted file mode 100644 index a0926451d..000000000 --- a/script.module.tubed.api/resources/language/resource.language.bs_ba/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: bs_ba\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ca_es/strings.po b/script.module.tubed.api/resources/language/resource.language.ca_es/strings.po deleted file mode 100644 index 2c88c980b..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ca_es/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Catalan (Spain) \n" -"Language: ca_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depuració" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.cs_cz/strings.po b/script.module.tubed.api/resources/language/resource.language.cs_cz/strings.po deleted file mode 100644 index 82af6a216..000000000 --- a/script.module.tubed.api/resources/language/resource.language.cs_cz/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-29 13:43+0000\n" -"Last-Translator: Kryštof Černý \n" -"Language-Team: Czech \n" -"Language: cs_cz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Doplněk pro přístup k YouTube Data API" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Doplněk API Tubed poskytuje pohodlný způsob přístupu k YouTube Data API pro Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Nastavte HTTP server, který bude poskytovat MPEG-DASH manifesty" - -msgctxt "#30002" -msgid "Port" -msgstr "Port" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Nastavte port HTTP serveru, který bude poskytovat MPEG-DASH manifesty" - -msgctxt "#30004" -msgid "Debug" -msgstr "Ladit" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Generátor manifestů" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.cy_gb/strings.po b/script.module.tubed.api/resources/language/resource.language.cy_gb/strings.po deleted file mode 100644 index db34024d3..000000000 --- a/script.module.tubed.api/resources/language/resource.language.cy_gb/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Welsh (United Kingdom) \n" -"Language: cy_gb\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=(n==0) ? 0 : (n==1) ? 1 : (n==2) ? 2 : (n==3) ? 3 :(n==6) ? 4 : 5;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Dadfygio" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.da_dk/strings.po b/script.module.tubed.api/resources/language/resource.language.da_dk/strings.po deleted file mode 100644 index aeecfa68b..000000000 --- a/script.module.tubed.api/resources/language/resource.language.da_dk/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-08-18 09:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Danish \n" -"Language: da_dk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Et modul for adgang til YouTubes data-API" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Tubed API-modulet giver en praktisk måde at få adgang til YouTubes data-API i Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Indstil den HTTP-server, som MPEG-DASH-manifester ligger på" - -msgctxt "#30002" -msgid "Port" -msgstr "Port" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Indstil den HTTP-serverport, som MPEG-DASH-manifester ligger på" - -msgctxt "#30004" -msgid "Debug" -msgstr "Fejlfinding" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Komponentlogføring" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Manifestgenerator" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Aktivér logføring af mere detaljerede oplysninger fra manifestgeneratoren" diff --git a/script.module.tubed.api/resources/language/resource.language.de_de/strings.po b/script.module.tubed.api/resources/language/resource.language.de_de/strings.po deleted file mode 100644 index 4ff7b9224..000000000 --- a/script.module.tubed.api/resources/language/resource.language.de_de/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: script.module.tubed.api\n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2023-02-06 23:15+0000\n" -"Last-Translator: Kai Sommerfeld \n" -"Language-Team: German \n" -"Language: de_de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.15.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Ein Modul für den Zugriff auf die YouTube-Daten-API" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Das Tubed-API-Modul stellt eine komfortable Möglichkeit zum Zugriff auf die YouTube-Daten-API in Kodi 19+ zur Verfügung" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "HTTP-Server konfigurieren, über den MPEG-DASH-Manifeste bereitgestellt werden" - -msgctxt "#30002" -msgid "Port" -msgstr "Port" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Port des HTTP-Servers konfigurieren, über den MPEG-DASH-Manifeste bereitgestellt werden" - -msgctxt "#30004" -msgid "Debug" -msgstr "Fehlersuche" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Komponentenprotokollierung" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Manifest-Generator" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Protokollieren detaillierterer Informationen für den Manifest-Generator aktivieren" diff --git a/script.module.tubed.api/resources/language/resource.language.el_gr/strings.po b/script.module.tubed.api/resources/language/resource.language.el_gr/strings.po deleted file mode 100644 index 4ccb10f1d..000000000 --- a/script.module.tubed.api/resources/language/resource.language.el_gr/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: el_gr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Μονάδα για προσπέλαση του API δεδομένων του Youtube" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Η μονάδα Tubed API παρέχει έναν βολικό τρόπο προσπέλασης του API δεδομένων του Youtube στο Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Ρύθμιση του εξυπηρετητή HTTP για τα μανιφέστα MPEG-DASH που φιλοξενούνται" - -msgctxt "#30002" -msgid "Port" -msgstr "Θύρα" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Ρύθμιση της θύρας του εξυπηρετητή HTTP στον οποίο φιλοξενούνται τα μανιφέστα MPEG-DASH" - -msgctxt "#30004" -msgid "Debug" -msgstr "Αποσφαλμάτωση" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Καταγραφή στοιχείων" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Γεννήτρια μανιφέστων" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Ενεργοποίηση καταγραφής σφαλμάτων για πιο λεπτομερείς πληροφορίες που αφορουν την γεννήτρια μανιφέστων" diff --git a/script.module.tubed.api/resources/language/resource.language.en_au/strings.po b/script.module.tubed.api/resources/language/resource.language.en_au/strings.po deleted file mode 100644 index 3a5ef6fb7..000000000 --- a/script.module.tubed.api/resources/language/resource.language.en_au/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-06-13 10:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: English (Australia) \n" -"Language: en_au\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.6.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "A module to access YouTube's Data API" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.en_gb/strings.po b/script.module.tubed.api/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index c1862aef8..000000000 --- a/script.module.tubed.api/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE\n" -"Language: en_GB\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.en_nz/strings.po b/script.module.tubed.api/resources/language/resource.language.en_nz/strings.po deleted file mode 100644 index 89528f869..000000000 --- a/script.module.tubed.api/resources/language/resource.language.en_nz/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: en_nz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.en_us/strings.po b/script.module.tubed.api/resources/language/resource.language.en_us/strings.po deleted file mode 100644 index 2f4b9b6be..000000000 --- a/script.module.tubed.api/resources/language/resource.language.en_us/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-06-13 10:29+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: English (United States) \n" -"Language: en_us\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.6.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "A module to access YouTube's Data API" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.eo/strings.po b/script.module.tubed.api/resources/language/resource.language.eo/strings.po deleted file mode 100644 index 2d24ff44d..000000000 --- a/script.module.tubed.api/resources/language/resource.language.eo/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: eo\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.es_ar/strings.po b/script.module.tubed.api/resources/language/resource.language.es_ar/strings.po deleted file mode 100644 index ae75a94dc..000000000 --- a/script.module.tubed.api/resources/language/resource.language.es_ar/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Spanish (Argentina) \n" -"Language: es_ar\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depuración" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.es_es/strings.po b/script.module.tubed.api/resources/language/resource.language.es_es/strings.po deleted file mode 100644 index 6d450c8fb..000000000 --- a/script.module.tubed.api/resources/language/resource.language.es_es/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2020-12-26 19:28+0000\n" -"Last-Translator: bsoriano \n" -"Language-Team: Spanish (Spain) \n" -"Language: es_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.4\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Un módulo para accedee al API de Datos de YouTube" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "El módulo API Tubed provee una manera conveniente de acceder al API de Datos de YouTube en Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Configurar el servidor HTTP en el cual están hosteados los manifests MPEG-DASH" - -msgctxt "#30002" -msgid "Port" -msgstr "Puerto" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Configurar el puerto del servidor HTTP en el cual están hosteados los manifests MPEG-DASH" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depurar" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Bitácora de Componentes" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Generador de Manifest" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Activar bitácora para información más detallada del generador de manifest" diff --git a/script.module.tubed.api/resources/language/resource.language.es_mx/strings.po b/script.module.tubed.api/resources/language/resource.language.es_mx/strings.po deleted file mode 100644 index 487b14ede..000000000 --- a/script.module.tubed.api/resources/language/resource.language.es_mx/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Spanish (Mexico) \n" -"Language: es_mx\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depurar" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.et_ee/strings.po b/script.module.tubed.api/resources/language/resource.language.et_ee/strings.po deleted file mode 100644 index ed77e876a..000000000 --- a/script.module.tubed.api/resources/language/resource.language.et_ee/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-11 09:00+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Estonian \n" -"Language: et_ee\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Silumine" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.eu_es/strings.po b/script.module.tubed.api/resources/language/resource.language.eu_es/strings.po deleted file mode 100644 index 55462c937..000000000 --- a/script.module.tubed.api/resources/language/resource.language.eu_es/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Basque (Spain) \n" -"Language: eu_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Araztu" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.fa_af/strings.po b/script.module.tubed.api/resources/language/resource.language.fa_af/strings.po deleted file mode 100644 index f13a6780b..000000000 --- a/script.module.tubed.api/resources/language/resource.language.fa_af/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: fa_af\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.fa_ir/strings.po b/script.module.tubed.api/resources/language/resource.language.fa_ir/strings.po deleted file mode 100644 index dad977b69..000000000 --- a/script.module.tubed.api/resources/language/resource.language.fa_ir/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Persian (Iran) \n" -"Language: fa_ir\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "دیباگ" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.fi_fi/strings.po b/script.module.tubed.api/resources/language/resource.language.fi_fi/strings.po deleted file mode 100644 index 0ff51ddc5..000000000 --- a/script.module.tubed.api/resources/language/resource.language.fi_fi/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Oskari Lavinto \n" -"Language-Team: Finnish \n" -"Language: fi_fi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Moduuli YouTube Data -rajapinnan käyttöön" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Tubed API-moduuli tarjoaa helpon tavan YouTube Data -rajapinnan käyttöön Kodi versioissa 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Määritä HTTP-palvelin, jolta MPEG-DASH-manifestit jaellaan" - -msgctxt "#30002" -msgid "Port" -msgstr "Portti" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Määritä HTTP-palvelin, jolta MPEG-DASH-manifestit jaellaan" - -msgctxt "#30004" -msgid "Debug" -msgstr "Vianselvitys" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Komponenttien kirjaus" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Manifestigeneraattori" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Käytä manifestigeneraattorille laajempaa lokikirjausta" diff --git a/script.module.tubed.api/resources/language/resource.language.fo_fo/strings.po b/script.module.tubed.api/resources/language/resource.language.fo_fo/strings.po deleted file mode 100644 index 1f239c0cf..000000000 --- a/script.module.tubed.api/resources/language/resource.language.fo_fo/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: fo_fo\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.fr_ca/strings.po b/script.module.tubed.api/resources/language/resource.language.fr_ca/strings.po deleted file mode 100644 index 1dfb54433..000000000 --- a/script.module.tubed.api/resources/language/resource.language.fr_ca/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-17 10:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: French (Canada) \n" -"Language: fr_ca\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Débogage" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.fr_fr/strings.po b/script.module.tubed.api/resources/language/resource.language.fr_fr/strings.po deleted file mode 100644 index a2aa58455..000000000 --- a/script.module.tubed.api/resources/language/resource.language.fr_fr/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2023-02-25 18:15+0000\n" -"Last-Translator: skypichat \n" -"Language-Team: French (France) \n" -"Language: fr_fr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.15.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Déboguer" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.gl_es/strings.po b/script.module.tubed.api/resources/language/resource.language.gl_es/strings.po deleted file mode 100644 index 9e2194609..000000000 --- a/script.module.tubed.api/resources/language/resource.language.gl_es/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Galician (Spain) \n" -"Language: gl_es\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depurar" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.he_il/strings.po b/script.module.tubed.api/resources/language/resource.language.he_il/strings.po deleted file mode 100644 index 1342eeb43..000000000 --- a/script.module.tubed.api/resources/language/resource.language.he_il/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Hebrew (Israel) \n" -"Language: he_il\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && n % 10 == 0) ? 2 : 3));\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "ניפוי שגיאות" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.hi_in/strings.po b/script.module.tubed.api/resources/language/resource.language.hi_in/strings.po deleted file mode 100644 index fe967f8fd..000000000 --- a/script.module.tubed.api/resources/language/resource.language.hi_in/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: hi_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.hr_hr/strings.po b/script.module.tubed.api/resources/language/resource.language.hr_hr/strings.po deleted file mode 100644 index 40e6bde05..000000000 --- a/script.module.tubed.api/resources/language/resource.language.hr_hr/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2023-01-14 18:15+0000\n" -"Last-Translator: gogogogi \n" -"Language-Team: Croatian \n" -"Language: hr_hr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.15\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Modul za pristup YouTubovom API-ju" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Tubed API modul omogućuje pouzdan način pristupa YouTubovom API-ju u Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Prilagodi HTTP poslužitelj s kojeg se hostiraju MPEG-DASH manifesti" - -msgctxt "#30002" -msgid "Port" -msgstr "Ulaz" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "HTTP ulaz poslužitelja s kojeg se hostiraju MPEG-DASH manifesti" - -msgctxt "#30004" -msgid "Debug" -msgstr "Otklanjanje grešaka" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Zapisivanje komponenta" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Generator manifesta" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Omogući zapisivanje više opširnijih informacija iz generatora manifesta" diff --git a/script.module.tubed.api/resources/language/resource.language.hu_hu/strings.po b/script.module.tubed.api/resources/language/resource.language.hu_hu/strings.po deleted file mode 100644 index 12bef4214..000000000 --- a/script.module.tubed.api/resources/language/resource.language.hu_hu/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Hungarian \n" -"Language: hu_hu\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Hibakeresés" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.hy_am/strings.po b/script.module.tubed.api/resources/language/resource.language.hy_am/strings.po deleted file mode 100644 index c4a280860..000000000 --- a/script.module.tubed.api/resources/language/resource.language.hy_am/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: hy_am\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.id_id/strings.po b/script.module.tubed.api/resources/language/resource.language.id_id/strings.po deleted file mode 100644 index 4bf21c19a..000000000 --- a/script.module.tubed.api/resources/language/resource.language.id_id/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: id_id\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.is_is/strings.po b/script.module.tubed.api/resources/language/resource.language.is_is/strings.po deleted file mode 100644 index 2ed7bb986..000000000 --- a/script.module.tubed.api/resources/language/resource.language.is_is/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Icelandic \n" -"Language: is_is\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n % 10 != 1 || n % 100 == 11;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Aflúsa villur" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.it_it/strings.po b/script.module.tubed.api/resources/language/resource.language.it_it/strings.po deleted file mode 100644 index 3857fdd0e..000000000 --- a/script.module.tubed.api/resources/language/resource.language.it_it/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-10-29 09:15+0000\n" -"Last-Translator: Massimo Pissarello \n" -"Language-Team: Italian \n" -"Language: it_it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.14.1\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Un modulo per accedere ai dati API di YouTube" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Il modulo Tubed API fornisce un modo conveniente per accedere all'API dei dati di YouTube in Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Configura il server HTTP su cui sono ospitati i manifest MPEG-DASH" - -msgctxt "#30002" -msgid "Port" -msgstr "Porta" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Configura la porta del server HTTP su cui sono ospitati i manifest MPEG-DASH" - -msgctxt "#30004" -msgid "Debug" -msgstr "Debug" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Logging componenti" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Generatore manifest" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Abilita il logging di eventi più dettagliati dal generatore manifest" diff --git a/script.module.tubed.api/resources/language/resource.language.ja_jp/strings.po b/script.module.tubed.api/resources/language/resource.language.ja_jp/strings.po deleted file mode 100644 index 19e51f229..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ja_jp/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Japanese \n" -"Language: ja_jp\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "デバッグ" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.kn_in/strings.po b/script.module.tubed.api/resources/language/resource.language.kn_in/strings.po deleted file mode 100644 index 04d6f2c21..000000000 --- a/script.module.tubed.api/resources/language/resource.language.kn_in/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: kn_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ko_kr/strings.po b/script.module.tubed.api/resources/language/resource.language.ko_kr/strings.po deleted file mode 100644 index 81d4f3b87..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ko_kr/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-08-28 08:29+0000\n" -"Last-Translator: Minho Park \n" -"Language-Team: Korean \n" -"Language: ko_kr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.8\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "YouTube의 데이터 API에 액세스하기 위한 모듈" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Tubed API 모듈은 Kodi 19+에서 YouTube의 Data API에 액세스하는 편리한 방법을 제공합니다" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "MPEG-DASH 매니페스트가 호스팅되는 HTTP 서버 구성" - -msgctxt "#30002" -msgid "Port" -msgstr "포트" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "MPEG-DASH 매니페스트가 호스팅되는 HTTP 서버 포트 구성" - -msgctxt "#30004" -msgid "Debug" -msgstr "디버그" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "구성 요소 로그" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "매니페스트 생성기" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "매니페스트 생성기에서 더 자세한 정보 로그 사용" diff --git a/script.module.tubed.api/resources/language/resource.language.lt_lt/strings.po b/script.module.tubed.api/resources/language/resource.language.lt_lt/strings.po deleted file mode 100644 index 33ffc9d19..000000000 --- a/script.module.tubed.api/resources/language/resource.language.lt_lt/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Lithuanian \n" -"Language: lt_lt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Derinimas" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.lv_lv/strings.po b/script.module.tubed.api/resources/language/resource.language.lv_lv/strings.po deleted file mode 100644 index d14f25c72..000000000 --- a/script.module.tubed.api/resources/language/resource.language.lv_lv/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Latvian \n" -"Language: lv_lv\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Atkļūdošana" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.mi/strings.po b/script.module.tubed.api/resources/language/resource.language.mi/strings.po deleted file mode 100644 index cf51c3ed1..000000000 --- a/script.module.tubed.api/resources/language/resource.language.mi/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.mk_mk/strings.po b/script.module.tubed.api/resources/language/resource.language.mk_mk/strings.po deleted file mode 100644 index 3e3631d03..000000000 --- a/script.module.tubed.api/resources/language/resource.language.mk_mk/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Macedonian \n" -"Language: mk_mk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n==1 || n%10==1 ? 0 : 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Дебаг" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ml_in/strings.po b/script.module.tubed.api/resources/language/resource.language.ml_in/strings.po deleted file mode 100644 index 111d1a6b2..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ml_in/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ml_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.mn_mn/strings.po b/script.module.tubed.api/resources/language/resource.language.mn_mn/strings.po deleted file mode 100644 index a8fec76d7..000000000 --- a/script.module.tubed.api/resources/language/resource.language.mn_mn/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mn_mn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ms_my/strings.po b/script.module.tubed.api/resources/language/resource.language.ms_my/strings.po deleted file mode 100644 index b84ba4af3..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ms_my/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Malay \n" -"Language: ms_my\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Nyahpepijat" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.mt_mt/strings.po b/script.module.tubed.api/resources/language/resource.language.mt_mt/strings.po deleted file mode 100644 index fa31908a4..000000000 --- a/script.module.tubed.api/resources/language/resource.language.mt_mt/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: mt_mt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.my_mm/strings.po b/script.module.tubed.api/resources/language/resource.language.my_mm/strings.po deleted file mode 100644 index aee3135b7..000000000 --- a/script.module.tubed.api/resources/language/resource.language.my_mm/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Burmese \n" -"Language: my_mm\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "အပြစ်ရှာဖွေရန်" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.nb_no/strings.po b/script.module.tubed.api/resources/language/resource.language.nb_no/strings.po deleted file mode 100644 index 1168ef25d..000000000 --- a/script.module.tubed.api/resources/language/resource.language.nb_no/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-27 01:17+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Norwegian Bokmål \n" -"Language: nb_no\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Feilsøk" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.nl_nl/strings.po b/script.module.tubed.api/resources/language/resource.language.nl_nl/strings.po deleted file mode 100644 index bec71610b..000000000 --- a/script.module.tubed.api/resources/language/resource.language.nl_nl/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-11 09:00+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Dutch \n" -"Language: nl_nl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Foutregistrering" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.oc_fr/strings.po b/script.module.tubed.api/resources/language/resource.language.oc_fr/strings.po deleted file mode 100644 index e1150725d..000000000 --- a/script.module.tubed.api/resources/language/resource.language.oc_fr/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: oc_fr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.os_os/strings.po b/script.module.tubed.api/resources/language/resource.language.os_os/strings.po deleted file mode 100644 index f6016e11e..000000000 --- a/script.module.tubed.api/resources/language/resource.language.os_os/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: os_os\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.pl_pl/strings.po b/script.module.tubed.api/resources/language/resource.language.pl_pl/strings.po deleted file mode 100644 index f81d262a7..000000000 --- a/script.module.tubed.api/resources/language/resource.language.pl_pl/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-30 09:46+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Polish \n" -"Language: pl_pl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Diagnostyka" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.pt_br/strings.po b/script.module.tubed.api/resources/language/resource.language.pt_br/strings.po deleted file mode 100644 index 62e8311a9..000000000 --- a/script.module.tubed.api/resources/language/resource.language.pt_br/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-30 09:46+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Portuguese (Brazil) \n" -"Language: pt_br\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depurar" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.pt_pt/strings.po b/script.module.tubed.api/resources/language/resource.language.pt_pt/strings.po deleted file mode 100644 index 13b4e7a65..000000000 --- a/script.module.tubed.api/resources/language/resource.language.pt_pt/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Portuguese (Portugal) \n" -"Language: pt_pt\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depuração" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ro_ro/strings.po b/script.module.tubed.api/resources/language/resource.language.ro_ro/strings.po deleted file mode 100644 index 32a33cc04..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ro_ro/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Romanian \n" -"Language: ro_ro\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Depanare" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ru_ru/strings.po b/script.module.tubed.api/resources/language/resource.language.ru_ru/strings.po deleted file mode 100644 index f242874b9..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ru_ru/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-03-12 13:18+0000\n" -"Last-Translator: Dmitry Petrov \n" -"Language-Team: Russian \n" -"Language: ru_ru\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.5.1\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Модуль для доступа к YouTube Data API" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "API модуль Tubed предоставляет удобный доступ к YouTube Data API в Kodi 19+" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "Настройте HTTP сервер, в котором размещаются MPEG-DASH манифесты" - -msgctxt "#30002" -msgid "Port" -msgstr "Порт" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "Настройте порт HTTP сервера, в котором размещаются MPEG-DASH манифесты" - -msgctxt "#30004" -msgid "Debug" -msgstr "Отладка" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "Отслеживание компонентов" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "Генератор манифестов" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "Включить сбор детальной информации из генератора манифестов" diff --git a/script.module.tubed.api/resources/language/resource.language.si_lk/strings.po b/script.module.tubed.api/resources/language/resource.language.si_lk/strings.po deleted file mode 100644 index 948a7d1dd..000000000 --- a/script.module.tubed.api/resources/language/resource.language.si_lk/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: si_lk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.sk_sk/strings.po b/script.module.tubed.api/resources/language/resource.language.sk_sk/strings.po deleted file mode 100644 index 9bc3fa3ba..000000000 --- a/script.module.tubed.api/resources/language/resource.language.sk_sk/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-12-07 09:13+0000\n" -"Last-Translator: Dušan Kazik \n" -"Language-Team: Slovak \n" -"Language: sk_sk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 4.9.1\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "Modul na prístup k údajom API služby Youtube" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "Port" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Ladenie" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.sl_si/strings.po b/script.module.tubed.api/resources/language/resource.language.sl_si/strings.po deleted file mode 100644 index 26f6b6e20..000000000 --- a/script.module.tubed.api/resources/language/resource.language.sl_si/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Slovenian \n" -"Language: sl_si\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Razhrošči" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.sq_al/strings.po b/script.module.tubed.api/resources/language/resource.language.sq_al/strings.po deleted file mode 100644 index bae0eae9c..000000000 --- a/script.module.tubed.api/resources/language/resource.language.sq_al/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: sq_al\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.sr_rs/strings.po b/script.module.tubed.api/resources/language/resource.language.sr_rs/strings.po deleted file mode 100644 index c5b9da5fc..000000000 --- a/script.module.tubed.api/resources/language/resource.language.sr_rs/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-30 09:46+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Serbian \n" -"Language: sr_rs\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Тражење грешака" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.sr_rs@latin/strings.po b/script.module.tubed.api/resources/language/resource.language.sr_rs@latin/strings.po deleted file mode 100644 index 671fb6329..000000000 --- a/script.module.tubed.api/resources/language/resource.language.sr_rs@latin/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Serbian (latin) \n" -"Language: sr_rs@latin\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Traženje grešaka" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.sv_se/strings.po b/script.module.tubed.api/resources/language/resource.language.sv_se/strings.po deleted file mode 100644 index 806860d3b..000000000 --- a/script.module.tubed.api/resources/language/resource.language.sv_se/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Swedish \n" -"Language: sv_se\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Felsök" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.szl/strings.po b/script.module.tubed.api/resources/language/resource.language.szl/strings.po deleted file mode 100644 index 77ff862fe..000000000 --- a/script.module.tubed.api/resources/language/resource.language.szl/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Silesian \n" -"Language: szl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Diagnostyka" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.ta_in/strings.po b/script.module.tubed.api/resources/language/resource.language.ta_in/strings.po deleted file mode 100644 index 60b3dc0ad..000000000 --- a/script.module.tubed.api/resources/language/resource.language.ta_in/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Tamil (India) \n" -"Language: ta_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "வழுநீக்கு" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.te_in/strings.po b/script.module.tubed.api/resources/language/resource.language.te_in/strings.po deleted file mode 100644 index 16259a311..000000000 --- a/script.module.tubed.api/resources/language/resource.language.te_in/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: te_in\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.tg_tj/strings.po b/script.module.tubed.api/resources/language/resource.language.tg_tj/strings.po deleted file mode 100644 index 932c13f77..000000000 --- a/script.module.tubed.api/resources/language/resource.language.tg_tj/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: tg_tj\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.th_th/strings.po b/script.module.tubed.api/resources/language/resource.language.th_th/strings.po deleted file mode 100644 index 4a366a110..000000000 --- a/script.module.tubed.api/resources/language/resource.language.th_th/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Thai \n" -"Language: th_th\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "แก้จุดบกพร่อง" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.tr_tr/strings.po b/script.module.tubed.api/resources/language/resource.language.tr_tr/strings.po deleted file mode 100644 index ab621cbdb..000000000 --- a/script.module.tubed.api/resources/language/resource.language.tr_tr/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Turkish \n" -"Language: tr_tr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Hata Ayıkla" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.uk_ua/strings.po b/script.module.tubed.api/resources/language/resource.language.uk_ua/strings.po deleted file mode 100644 index c66d6fe76..000000000 --- a/script.module.tubed.api/resources/language/resource.language.uk_ua/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Ukrainian \n" -"Language: uk_ua\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Відладка" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.uz_uz/strings.po b/script.module.tubed.api/resources/language/resource.language.uz_uz/strings.po deleted file mode 100644 index 5678c39bd..000000000 --- a/script.module.tubed.api/resources/language/resource.language.uz_uz/strings.po +++ /dev/null @@ -1,53 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: uz_uz\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.vi_vn/strings.po b/script.module.tubed.api/resources/language/resource.language.vi_vn/strings.po deleted file mode 100644 index b536821a1..000000000 --- a/script.module.tubed.api/resources/language/resource.language.vi_vn/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-04-08 00:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Vietnamese \n" -"Language: vi_vn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.11.2\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "Gỡ lỗi" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/language/resource.language.zh_cn/strings.po b/script.module.tubed.api/resources/language/resource.language.zh_cn/strings.po deleted file mode 100644 index 4c0154e1a..000000000 --- a/script.module.tubed.api/resources/language/resource.language.zh_cn/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2021-11-09 05:30+0000\n" -"Last-Translator: taxigps \n" -"Language-Team: Chinese (China) \n" -"Language: zh_cn\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.8.1\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "访问 YouTube 数据 API 的模块" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "Tubed API 模块为 Kodi 19+ 提供了访问 YouTube 数据 API 的便捷方式" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "MPEG-DASH HTTPD" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "配置承载 MPEG-DASH 指令清单的 HTTP 服务器" - -msgctxt "#30002" -msgid "Port" -msgstr "端口" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "配置承载 MPEG-DASH 指令清单的 HTTP 服务器端口" - -msgctxt "#30004" -msgid "Debug" -msgstr "调试" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "组件日志记录" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "指令清单生成器" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "启用从指令清单生成器记录更多详细日志信息" diff --git a/script.module.tubed.api/resources/language/resource.language.zh_tw/strings.po b/script.module.tubed.api/resources/language/resource.language.zh_tw/strings.po deleted file mode 100644 index 9348496cc..000000000 --- a/script.module.tubed.api/resources/language/resource.language.zh_tw/strings.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: translations@kodi.tv\n" -"POT-Creation-Date: 2016-09-18 00:54+0000\n" -"PO-Revision-Date: 2022-03-01 17:13+0000\n" -"Last-Translator: Christian Gade \n" -"Language-Team: Chinese (Taiwan) \n" -"Language: zh_tw\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.11\n" - -msgctxt "Addon Summary" -msgid "A module to access YouTube's Data API" -msgstr "" - -msgctxt "Addon Description" -msgid "The Tubed API module provides a convenient way to access YouTube's Data API in Kodi 19+" -msgstr "" - -msgctxt "#30000" -msgid "MPEG-DASH HTTPD" -msgstr "" - -msgctxt "#30001" -msgid "Configure the HTTP server that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30002" -msgid "Port" -msgstr "" - -msgctxt "#30003" -msgid "Configure the HTTP server port that MPEG-DASH manifests are hosted from" -msgstr "" - -msgctxt "#30004" -msgid "Debug" -msgstr "除錯" - -msgctxt "#30005" -msgid "Component Logging" -msgstr "" - -msgctxt "#30006" -msgid "Manifest generator" -msgstr "" - -msgctxt "#30007" -msgid "Enable logging of more detailed information from the manifest generator" -msgstr "" diff --git a/script.module.tubed.api/resources/lib/__init__.py b/script.module.tubed.api/resources/lib/__init__.py deleted file mode 100644 index df2b3887d..000000000 --- a/script.module.tubed.api/resources/lib/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -__all__ = ['src', 'service'] diff --git a/script.module.tubed.api/resources/lib/service.py b/script.module.tubed.api/resources/lib/service.py deleted file mode 100644 index ccc15ba66..000000000 --- a/script.module.tubed.api/resources/lib/service.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from src import service # pylint: disable=import-error - -service.invoke() diff --git a/script.module.tubed.api/resources/lib/src/__init__.py b/script.module.tubed.api/resources/lib/src/__init__.py deleted file mode 100644 index a04a02833..000000000 --- a/script.module.tubed.api/resources/lib/src/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -__all__ = ['constants', 'httpd', 'tubed_api', 'service'] diff --git a/script.module.tubed.api/resources/lib/src/constants/__init__.py b/script.module.tubed.api/resources/lib/src/constants/__init__.py deleted file mode 100644 index 3454e9ee0..000000000 --- a/script.module.tubed.api/resources/lib/src/constants/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from .config import ADDON_ID -from .config import TEMP_DIRECTORY - -# the actual constants -__all__ = ['ADDON_ID', 'TEMP_DIRECTORY'] - -# the modules containing the constants -__all__ += ['config'] diff --git a/script.module.tubed.api/resources/lib/src/constants/config.py b/script.module.tubed.api/resources/lib/src/constants/config.py deleted file mode 100644 index aae2dc79d..000000000 --- a/script.module.tubed.api/resources/lib/src/constants/config.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -ADDON_ID = 'script.module.tubed.api' -TEMP_DIRECTORY = 'special://temp/%s/' % ADDON_ID diff --git a/script.module.tubed.api/resources/lib/src/httpd/__init__.py b/script.module.tubed.api/resources/lib/src/httpd/__init__.py deleted file mode 100644 index b06182960..000000000 --- a/script.module.tubed.api/resources/lib/src/httpd/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -__all__ = ['daemon', 'handler'] diff --git a/script.module.tubed.api/resources/lib/src/httpd/daemon.py b/script.module.tubed.api/resources/lib/src/httpd/daemon.py deleted file mode 100644 index 295fac02c..000000000 --- a/script.module.tubed.api/resources/lib/src/httpd/daemon.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from http.server import HTTPServer -from threading import Thread - -import requests -import xbmc # pylint: disable=import-error -import xbmcaddon # pylint: disable=import-error -import xbmcvfs # pylint: disable=import-error - -from ..constants import ADDON_ID -from ..constants import TEMP_DIRECTORY -from .handler import RequestHandler - - -class HTTPDaemon(xbmc.Monitor): - cache_path = xbmcvfs.translatePath(TEMP_DIRECTORY) - - def __init__(self): - self._address = '127.0.0.1' - self._port = xbmcaddon.Addon(ADDON_ID).getSettingInt('httpd.port') or 52520 - - self._httpd = None - self._thread = None - - super().__init__() - - @property - def address(self): - return self._address - - @property - def port(self): - return int(self._port) - - @property - def httpd(self): - return self._httpd - - @httpd.setter - def httpd(self, value): - self._httpd = value - - @property - def thread(self): - return self._thread - - @thread.setter - def thread(self, value): - self._thread = value - - def start(self): - if not self.httpd: - self.httpd = self._server() - self.thread = Thread(target=self.httpd.serve_forever) - self.thread.daemon = True - self.thread.start() - - def restart(self): - self.shutdown() - self.start() - - def shutdown(self): - if self.httpd: - self.httpd.shutdown() - self.httpd.socket.close() - self.thread.join() - self.thread = None - self.httpd = None - - def ping(self): - try: - response = requests.get('http://{address}:{port}/ping' - .format(address=self.address, port=self.port), - timeout=60) - return response.status_code == 204 - - finally: - return False # pylint: disable=lost-exception - - def clean_cache(self): - if xbmcvfs.exists(self.cache_path): - xbmcvfs.rmdir(self.cache_path, force=True) - - return not xbmcvfs.exists(self.cache_path) - - def _server(self): - server = HTTPServer((self.address, self.port), RequestHandler) - return server diff --git a/script.module.tubed.api/resources/lib/src/httpd/handler.py b/script.module.tubed.api/resources/lib/src/httpd/handler.py deleted file mode 100644 index 0da3b8d84..000000000 --- a/script.module.tubed.api/resources/lib/src/httpd/handler.py +++ /dev/null @@ -1,203 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -import os -import re -import traceback -from http.server import BaseHTTPRequestHandler -from ipaddress import ip_address - -import requests -import xbmc # pylint: disable=import-error -import xbmcgui # pylint: disable=import-error -import xbmcvfs # pylint: disable=import-error - -from ..constants import TEMP_DIRECTORY -from ..tubed_api.utils.logger import Log - -LOG = Log('httpd', __file__) - - -class RequestHandler(BaseHTTPRequestHandler): - cache_path = xbmcvfs.translatePath(TEMP_DIRECTORY) - chunk_size = 1024 * 64 - - def log_message(self, format, *args): # pylint: disable=redefined-builtin - return # disable BaseHTTPRequestHandler logging mechanism - - def connection_allowed(self): - return ip_address(self.client_address[0]).is_private - - def _get_path(self): - return os.path.join(self.cache_path, self.path.strip('/').strip('\\')) - - def _get_chunks(self, data): - for i in range(0, len(data), self.chunk_size): - yield data[i:i + self.chunk_size] - - def do_GET(self): # pylint: disable=invalid-name - if not self.connection_allowed(): - LOG.warning('Rejected GET connection from %s' % - ip_address(self.client_address[0])) - self.send_error(403) - return - - if self.path.endswith('.mpd'): - dash_manifest = self._get_path() - LOG.debug('A manifest was requested @ %s' % self.path) - - self.send_response(200) - self.send_header('Content-Type', 'application/xml+dash') - self.send_header('Content-Length', str(os.path.getsize(dash_manifest))) - self.end_headers() - - try: - with open(dash_manifest, 'rb') as file_handle: - chunk = True - while chunk: - chunk = file_handle.read(self.chunk_size) - if chunk: - self.wfile.write(chunk) - - LOG.debug('A manifest was delivered from %s' % self.path) - - except IOError: - LOG.error('An IOError occurred when retrieving the manifest @ %s' % self.path) - LOG.error(traceback.format_exc()) - self.send_error(404) - - except: # pylint: disable=bare-except - LOG.error('An unknown occurred when retrieving the manifest @ %s' % self.path) - LOG.error(traceback.format_exc()) - self.send_error(500) - - elif self.path == '/ping': - self.send_error(204) - - else: - LOG.error('Rejected GET connection to an unknown resource @ %s' % self.path) - self.send_error(501) - - def do_HEAD(self): # pylint: disable=invalid-name - if not self.connection_allowed(): - LOG.warning('Rejected HEAD connection from %s' % - ip_address(self.client_address[0])) - self.send_error(403) - return - - if self.path.endswith('.mpd'): - dash_manifest = self._get_path() - - LOG.debug('A manifest existence check was requested @ %s' % self.path) - - if not os.path.isfile(dash_manifest): - LOG.error('A manifest does not exist @ %s' % self.path) - self.send_error(404) - return - - self.send_response(200) - self.send_header('Content-Type', 'application/xml+dash') - self.send_header('Content-Length', str(os.path.getsize(dash_manifest))) - self.end_headers() - - LOG.error('A manifest exists @ %s' % self.path) - return - - LOG.error('Rejected HEAD connection to an unknown resource @ %s' % self.path) - self.send_error(501) - - def do_POST(self): # pylint: disable=invalid-name - if not self.connection_allowed(): - LOG.warning('Rejected POST connection from %s' % - ip_address(self.client_address[0])) - self.send_error(403) - return - - if self.path.startswith('/widevine'): - LOG.debug('A widevine license request was made') - - url = xbmcgui.Window(10000).getProperty('tubed-api-license_url') - token = xbmcgui.Window(10000).getProperty('tubed-api-license_token') - - if not url: - LOG.error('No license url was found') - self.send_error(404) - return - - if not token: - LOG.error('No license token was found') - self.send_error(403) - return - - size_limit = None - - length = int(self.headers['Content-Length']) - data = self.rfile.read(length) - - headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'Bearer %s' % token - } - - response = requests.post(url=url, headers=headers, - data=data, stream=True, - timeout=60) - - content = response.raw.read(int(response.headers.get('content-length'))) - - header, body = content.split('\r\n\r\n'.encode('utf-8')) - header = header.decode('utf-8') - - authorized_formats = re.search(r'^Authorized-Format-Types:\s*(?P.+?)\r*$', - header, re.MULTILINE) - if authorized_formats: - authorized_types = authorized_formats.group('types').split(',') - - formats = { - 'SD': (1280 * 528) - 1, - 'HD720': 1280 * 720, - 'HD': 7680 * 4320 - } - - if 'HD' in authorized_types: - size_limit = formats['HD'] - - elif 'HD720' in authorized_types: - if xbmc.getCondVisibility('system.platform.android') == 1: - size_limit = formats['HD720'] - else: - size_limit = formats['SD'] - - elif 'SD' in authorized_types: - size_limit = formats['SD'] - - self.send_response(200) - - if size_limit: - self.send_header('X-Limit-Video', 'max={size_limit}px' - .format(size_limit=str(size_limit))) - - for header, value in response.headers.items(): - if value.lower() == 'content-length': - self.send_header(value, str(len(body))) - - else: - self.send_header(header, value) - - self.end_headers() - - for chunk in self._get_chunks(body): - self.wfile.write(chunk) - - LOG.debug('A widevine license response was sent') - return - - LOG.error('Rejected POST connection to an unknown resource @ %s' % self.path) - self.send_error(501) diff --git a/script.module.tubed.api/resources/lib/src/service.py b/script.module.tubed.api/resources/lib/src/service.py deleted file mode 100644 index a23a0b4b8..000000000 --- a/script.module.tubed.api/resources/lib/src/service.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from .httpd.daemon import HTTPDaemon -from .tubed_api.utils.time import now -from .tubed_api.utils.time import timestamp_diff - - -def invoke(): - sleep_time = 10 - ping_delay = 60 - ping_timestamp = None - - httpd = HTTPDaemon() - - httpd.clean_cache() - - httpd.start() - - while not httpd.abortRequested(): - - if ping_timestamp is None or timestamp_diff(ping_timestamp) >= ping_delay: - ping_timestamp = str(now()) - - if not httpd.ping(): - httpd.restart() - - if httpd.waitForAbort(sleep_time): - break - - if httpd: - httpd.shutdown() diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/__init__.py deleted file mode 100644 index 5947f84bc..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -ACCESS_TOKEN = '' -API_KEY = '' -CLIENT_ID = '' -CLIENT_SECRET = '' -HTTP_REFERRER = '' - -ACCESS_TOKEN_TV = '' -API_KEY_TV = 'QUl6YVN5QzZmdlpTSkhBN1Z6NWo4akNpS1J0N3RVSU9xakUyTjNn' -CLIENT_ID_TV = 'ODYxNTU2NzA4NDU0LWQ2ZGxtM2xoMDVpZGQ4bnBlazE4azZiZThiYTNvYzY4' -CLIENT_SECRET_TV = 'U2JvVmhvRzlzMHJOYWZpeENTR0dLWEFU' - -__all__ = ['ACCESS_TOKEN', 'ACCESS_TOKEN_TV', 'API_KEY', 'API_KEY_TV', 'CLIENT_ID', - 'CLIENT_ID_TV', 'CLIENT_SECRET', 'HTTP_REFERRER', 'CLIENT_SECRET_TV', - 'constants', 'oauth', 'usher', 'utils', 'v3', 'exceptions'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/constants/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/constants/__init__.py deleted file mode 100644 index 7d1bc3742..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/constants/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2022 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from .http import MOBILE_HEADERS - -# the actual constants -__all__ = ['MOBILE_HEADERS'] - -# the modules containing the constants -__all__ += ['http'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/constants/http.py b/script.module.tubed.api/resources/lib/src/tubed_api/constants/http.py deleted file mode 100644 index f95ed8444..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/constants/http.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2022 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -MOBILE_HEADERS = { - 'User-Agent': ('Mozilla/5.0 (Linux; Android 7.0; SM-G892A Build/NRD90M;' - ' wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0' - ' Chrome/67.0.3396.87 Mobile Safari/537.36'), - 'Accept': '*/*', - 'DNT': '1', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'en-US,en;q=0.5' -} diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/exceptions.py b/script.module.tubed.api/resources/lib/src/tubed_api/exceptions.py deleted file mode 100644 index c09d23956..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/exceptions.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - - -class TubedAPIException(Exception): - _data = { - 'error': 'exception', - 'error_description': 'Unknown exception occurred', - 'code': '500' - } - - def __init__(self, data=None): - super().__init__() - - if isinstance(data, str) and data: - self._data['error_description'] = data - elif isinstance(data, dict): - self._data.update(data) - - @property - def data(self): - return self._data - - @property - def error(self): - return self.data.get('error', 'exception') - - @property - def description(self): - return self.data.get('description', 'Unknown exception occurred') - - @property - def code(self): - return self.data.get('code', '500') - - -class TubedOAuthException(TubedAPIException): - pass - - -class ResourceUnavailable(TubedAPIException): - pass - - -class ContentNoResponse(TubedAPIException): - pass - - -class ContentRestricted(TubedAPIException): - pass - - -class OAuthRequestFailed(TubedOAuthException): - pass - - -class OAuthInvalidGrant(TubedOAuthException): - pass diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/oauth/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/oauth/__init__.py deleted file mode 100644 index 173f8dbce..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/oauth/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from .client import Client - -__all__ = ['client', 'Client', 'scopes'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/oauth/client.py b/script.module.tubed.api/resources/lib/src/tubed_api/oauth/client.py deleted file mode 100644 index 96899605d..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/oauth/client.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -import requests - -from ..constants import MOBILE_HEADERS -from ..exceptions import OAuthInvalidGrant -from ..exceptions import OAuthRequestFailed -from . import scopes - - -class Client: - # https://developers.google.com/youtube/v3/guides/auth/devices - - def __init__(self, client_id='', client_secret=''): - # pylint: disable=import-outside-toplevel - - if client_id and client_secret: - self.client_id = client_id - self.client_secret = client_secret - else: - from .. import CLIENT_ID - from .. import CLIENT_SECRET - self.client_id = CLIENT_ID - self.client_secret = CLIENT_SECRET - - from .. import HTTP_REFERRER - self.http_referrer = HTTP_REFERRER - - def request_codes(self, scope=None): - headers = { - 'Host': 'accounts.google.com', - 'Content-Type': 'application/x-www-form-urlencoded' - } - headers.update(MOBILE_HEADERS.copy()) - - if self.http_referrer: - headers['Referer'] = self.http_referrer - - if not scope: - scope = scopes.YOUTUBE - - elif isinstance(scope, list): - scope = ' '.join(scope) - - data = { - 'client_id': self.client_id, - 'scope': scope - } - - response, payload = self._post('https://accounts.google.com/o/oauth2/device/code', - data=data, headers=headers) - - if 'error' in payload: - payload.update({ - 'code': str(response.status_code) - }) - raise OAuthRequestFailed(payload) - - if response.status_code != requests.codes.ok: # pylint: disable=no-member - raise OAuthRequestFailed({ - 'error': 'code_request_failed', - 'error_description': 'Code request failed with status code %s' % - str(response.status_code), - 'code': str(response.status_code) - }) - - if response.headers.get('content-type', '').startswith('application/json'): - return payload - - raise OAuthRequestFailed({ - 'error': 'code_request_failed_unknown', - 'error_description': 'Code request failed with an unknown response', - 'code': str(response.status_code) - }) - - def request_access_token(self, code): - headers = { - 'Host': 'www.googleapis.com', - 'Content-Type': 'application/x-www-form-urlencoded' - } - headers.update(MOBILE_HEADERS.copy()) - - if self.http_referrer: - headers['Referer'] = self.http_referrer - - data = { - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'code': code, - 'grant_type': 'http://oauth.net/grant_type/device/1.0' - } - - response, payload = self._post('https://www.googleapis.com/oauth2/v4/token', - data=data, headers=headers) - - pending = False - - if 'error' in payload: - pending = payload['error'] == 'authorization_pending' - - if not pending: - payload.update({ - 'code': str(response.status_code) - }) - raise OAuthRequestFailed(payload) - - if (response.status_code != requests.codes.ok) and not pending: # pylint: disable=no-member - raise OAuthRequestFailed({ - 'error': 'access_token_request_failed', - 'error_description': 'Access token request failed with status code %s' % - str(response.status_code), - 'code': str(response.status_code) - }) - - if response.headers.get('content-type', '').startswith('application/json'): - return payload - - raise OAuthRequestFailed({ - 'error': 'access_token_request_failed_unknown', - 'error_description': 'Access token request failed with an unknown response', - 'code': str(response.status_code) - }) - - def refresh_token(self, token): - headers = { - 'Host': 'www.googleapis.com', - 'Content-Type': 'application/x-www-form-urlencoded' - } - headers.update(MOBILE_HEADERS.copy()) - - if self.http_referrer: - headers['Referer'] = self.http_referrer - - data = { - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'refresh_token': token, - 'grant_type': 'refresh_token' - } - - response, payload = self._post('https://www.googleapis.com/oauth2/v4/token', - data=data, headers=headers) - - if 'error' in payload: - payload.update({ - 'code': str(response.status_code) - }) - - if payload['error'] == 'invalid_grant' and payload['code'] == '400': - raise OAuthInvalidGrant(payload) - - raise OAuthRequestFailed(payload) - - if response.status_code != requests.codes.ok: # pylint: disable=no-member - raise OAuthRequestFailed({ - 'error': 'refresh_token_request_failed', - 'error_description': 'Refreshing token failed with status code %s' % - str(response.status_code), - 'code': str(response.status_code) - }) - - if response.headers.get('content-type', '').startswith('application/json'): - return payload['access_token'], int(payload.get('expires_in', 3600)) - - return '', '' - - def revoke_token(self, token): - headers = { - 'Host': 'accounts.google.com', - 'Content-Type': 'application/x-www-form-urlencoded' - } - headers.update(MOBILE_HEADERS.copy()) - - if self.http_referrer: - headers['Referer'] = self.http_referrer - - data = { - 'token': token - } - - response, payload = self._post('https://accounts.google.com/o/oauth2/revoke', - data=data, headers=headers) - - if 'error' in payload: - payload.update({ - 'code': str(response.status_code) - }) - raise OAuthRequestFailed(payload) - - if response.status_code != requests.codes.ok: # pylint: disable=no-member - raise OAuthRequestFailed({ - 'error': 'revoke_token_request_failed', - 'error_description': 'Token revocation failed', - 'code': str(response.status_code) - }) - - @staticmethod - def _post(url, data, headers): - response = requests.post(url, data=data, headers=headers, timeout=60) - - response.encoding = 'utf-8' - - try: - return response, response.json() - except ValueError: - return response, {} diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/oauth/scopes.py b/script.module.tubed.api/resources/lib/src/tubed_api/oauth/scopes.py deleted file mode 100644 index 5809fc437..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/oauth/scopes.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -# Manage your YouTube account -YOUTUBE = 'https://www.googleapis.com/auth/youtube' - -# See a list of your current active channel members, their current level, and when they -# became a member -CHANNEL_MEMBERSHIPS = 'https://www.googleapis.com/auth/youtube.channel-memberships.creator' - -# See, edit, and permanently delete your YouTube videos, ratings, comments and captions -FORCE_SSL = 'https://www.googleapis.com/auth/youtube.force-ssl' - -# View your YouTube account -READONLY = 'https://www.googleapis.com/auth/youtube.readonly' - -# Manage your YouTube videos -UPLOAD = 'https://www.googleapis.com/auth/youtube.upload' - -# View and manage your assets and associated content on YouTube -PARTNER = 'https://www.googleapis.com/auth/youtubepartner' - -# View private information of your YouTube channel relevant during the audit process -# with a YouTube partner -PARTNER_CHANNEL_AUDIT = 'https://www.googleapis.com/auth/youtubepartner-channel-audit' diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/usher/__init__.py deleted file mode 100644 index 49b2a0201..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from .lib.quality import Quality -from .resolver import resolve - -__all__ = ['Quality', 'resolve', 'resolver'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/__init__.py deleted file mode 100644 index f6981a3c3..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -__all__ = ['mpeg_dash', 'quality', 'subtitles', 'video_info'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/itags.json b/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/itags.json deleted file mode 100644 index 4e7c81a4e..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/itags.json +++ /dev/null @@ -1,1160 +0,0 @@ -{ - "5": { - "container": "flv", - "title": "240p", - "sort": [ - 240, - 0 - ], - "video": { - "height": 240, - "encoding": "h.263" - }, - "audio": { - "bitrate": 64, - "encoding": "mp3" - } - }, - "6": { - "container": "flv", - "discontinued": true, - "video": { - "height": 270, - "encoding": "h.263" - }, - "audio": { - "bitrate": 64, - "encoding": "mp3" - } - }, - "13": { - "container": "3gp", - "discontinued": true, - "video": { - "encoding": "mpeg-4" - }, - "audio": { - "encoding": "aac" - } - }, - "17": { - "container": "3gp", - "title": "144p", - "sort": [ - 144, - -20 - ], - "video": { - "height": 144, - "encoding": "mpeg-4" - }, - "audio": { - "bitrate": 24, - "encoding": "aac" - } - }, - "18": { - "container": "mp4", - "title": "360p", - "sort": [ - 360, - 0 - ], - "video": { - "height": 360, - "encoding": "h.264" - }, - "audio": { - "bitrate": 96, - "encoding": "aac" - } - }, - "22": { - "container": "mp4", - "title": "720p", - "sort": [ - 720, - 0 - ], - "video": { - "height": 720, - "encoding": "h.264" - }, - "audio": { - "bitrate": 192, - "encoding": "aac" - } - }, - "34": { - "container": "flv", - "discontinued": true, - "video": { - "height": 360, - "encoding": "h.264" - }, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "35": { - "container": "flv", - "discontinued": true, - "video": { - "height": 480, - "encoding": "h.264" - }, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "36": { - "container": "3gp", - "title": "240p", - "sort": [ - 240, - -20 - ], - "video": { - "height": 240, - "encoding": "mpeg-4" - }, - "audio": { - "bitrate": 32, - "encoding": "aac" - } - }, - "37": { - "container": "mp4", - "title": "1080p", - "sort": [ - 1080, - 0 - ], - "video": { - "height": 1080, - "encoding": "h.264" - }, - "audio": { - "bitrate": 192, - "encoding": "aac" - } - }, - "38": { - "container": "mp4", - "title": "3072p", - "sort": [ - 3072, - 0 - ], - "video": { - "height": 3072, - "encoding": "h.264" - }, - "audio": { - "bitrate": 192, - "encoding": "aac" - } - }, - "43": { - "container": "webm", - "title": "360p", - "sort": [ - 360, - -1 - ], - "video": { - "height": 360, - "encoding": "vp8" - }, - "audio": { - "bitrate": 128, - "encoding": "vorbis" - } - }, - "44": { - "container": "webm", - "discontinued": true, - "video": { - "height": 480, - "encoding": "vp8" - }, - "audio": { - "bitrate": 128, - "encoding": "vorbis" - } - }, - "45": { - "container": "webm", - "discontinued": true, - "video": { - "height": 720, - "encoding": "vp8" - }, - "audio": { - "bitrate": 192, - "encoding": "vorbis" - } - }, - "46": { - "container": "webm", - "discontinued": true, - "video": { - "height": 1080, - "encoding": "vp8" - }, - "audio": { - "bitrate": 192, - "encoding": "vorbis" - } - }, - "59": { - "container": "mp4", - "title": "480p", - "sort": [ - 480, - 0 - ], - "video": { - "height": 480, - "encoding": "h.264" - }, - "audio": { - "bitrate": 96, - "encoding": "aac" - } - }, - "78": { - "container": "mp4", - "title": "360p", - "sort": [ - 360, - 0 - ], - "video": { - "height": 360, - "encoding": "h.264" - }, - "audio": { - "bitrate": 96, - "encoding": "aac" - } - }, - "82": { - "container": "mp4", - "3D": true, - "title": "3D@360p", - "sort": [ - 360, - 0 - ], - "video": { - "height": 360, - "encoding": "h.264" - }, - "audio": { - "bitrate": 96, - "encoding": "aac" - } - }, - "83": { - "container": "mp4", - "3D": true, - "title": "3D@240p", - "sort": [ - 240, - 0 - ], - "video": { - "height": 240, - "encoding": "h.264" - }, - "audio": { - "bitrate": 96, - "encoding": "aac" - } - }, - "84": { - "container": "mp4", - "3D": true, - "title": "3D@720p", - "sort": [ - 720, - 0 - ], - "video": { - "height": 720, - "encoding": "h.264" - }, - "audio": { - "bitrate": 192, - "encoding": "aac" - } - }, - "85": { - "container": "mp4", - "3D": true, - "title": "3D@1080p", - "sort": [ - 1080, - 0 - ], - "video": { - "height": 1080, - "encoding": "h.264" - }, - "audio": { - "bitrate": 192, - "encoding": "aac" - } - }, - "100": { - "container": "webm", - "3D": true, - "title": "3D@360p", - "sort": [ - 360, - -1 - ], - "video": { - "height": 360, - "encoding": "vp8" - }, - "audio": { - "bitrate": 128, - "encoding": "vorbis" - } - }, - "101": { - "container": "webm", - "discontinued": true, - "3D": true, - "title": "3D@360p", - "sort": [ - 360, - -1 - ], - "video": { - "height": 360, - "encoding": "vp8" - }, - "audio": { - "bitrate": 192, - "encoding": "vorbis" - } - }, - "102": { - "container": "webm", - "discontinued": true, - "3D": true, - "video": { - "height": 720, - "encoding": "vp8" - }, - "audio": { - "bitrate": 192, - "encoding": "vorbis" - } - }, - "91": { - "container": "ts", - "Live": true, - "title": "Live@144p", - "sort": [ - 144, - 0 - ], - "video": { - "height": 144, - "encoding": "h.264" - }, - "audio": { - "bitrate": 48, - "encoding": "aac" - } - }, - "92": { - "container": "ts", - "Live": true, - "title": "Live@240p", - "sort": [ - 240, - 0 - ], - "video": { - "height": 240, - "encoding": "h.264" - }, - "audio": { - "bitrate": 48, - "encoding": "aac" - } - }, - "93": { - "container": "ts", - "Live": true, - "title": "Live@360p", - "sort": [ - 360, - 0 - ], - "video": { - "height": 360, - "encoding": "h.264" - }, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "94": { - "container": "ts", - "Live": true, - "title": "Live@480p", - "sort": [ - 480, - 0 - ], - "video": { - "height": 480, - "encoding": "h.264" - }, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "95": { - "container": "ts", - "Live": true, - "title": "Live@720p", - "sort": [ - 720, - 0 - ], - "video": { - "height": 720, - "encoding": "h.264" - }, - "audio": { - "bitrate": 256, - "encoding": "aac" - } - }, - "96": { - "container": "ts", - "Live": true, - "title": "Live@1080p", - "sort": [ - 1080, - 0 - ], - "video": { - "height": 1080, - "encoding": "h.264" - }, - "audio": { - "bitrate": 256, - "encoding": "aac" - } - }, - "120": { - "container": "flv", - "discontinued": true, - "Live": true, - "title": "Live@720p", - "sort": [ - 720, - -10 - ], - "video": { - "height": 720, - "encoding": "h.264" - }, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "127": { - "container": "ts", - "Live": true, - "audio": { - "bitrate": 96, - "encoding": "aac" - } - }, - "128": { - "container": "ts", - "Live": true, - "audio": { - "bitrate": 96, - "encoding": "aac" - } - }, - "132": { - "container": "ts", - "Live": true, - "title": "Live@240p", - "sort": [ - 240, - 0 - ], - "video": { - "height": 240, - "encoding": "h.264" - }, - "audio": { - "bitrate": 48, - "encoding": "aac" - } - }, - "151": { - "container": "ts", - "Live": true, - "unsupported": true, - "title": "Live@72p", - "sort": [ - 72, - 0 - ], - "video": { - "height": 72, - "encoding": "h.264" - }, - "audio": { - "bitrate": 24, - "encoding": "aac" - } - }, - "300": { - "container": "ts", - "Live": true, - "title": "Live@720p", - "sort": [ - 720, - 0 - ], - "video": { - "height": 720, - "encoding": "h.264" - }, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "301": { - "container": "ts", - "Live": true, - "title": "Live@1080p", - "sort": [ - 1080, - 0 - ], - "video": { - "height": 1080, - "encoding": "h.264" - }, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "133": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 240, - "encoding": "h.264" - } - }, - "134": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 360, - "encoding": "h.264" - } - }, - "135": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 480, - "encoding": "h.264" - } - }, - "136": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 720, - "encoding": "h.264" - } - }, - "137": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 1080, - "encoding": "h.264" - } - }, - "138": { - "container": "mp4", - "discontinued": true, - "dash/video": true, - "video": { - "height": 2160, - "encoding": "h.264" - } - }, - "160": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 144, - "encoding": "h.264" - } - }, - "167": { - "container": "webm", - "dash/video": true, - "video": { - "height": 360, - "encoding": "vp8" - } - }, - "168": { - "container": "webm", - "dash/video": true, - "video": { - "height": 480, - "encoding": "vp8" - } - }, - "169": { - "container": "webm", - "dash/video": true, - "video": { - "height": 720, - "encoding": "vp8" - } - }, - "170": { - "container": "webm", - "dash/video": true, - "video": { - "height": 1080, - "encoding": "vp8" - } - }, - "218": { - "container": "webm", - "dash/video": true, - "video": { - "height": 480, - "encoding": "vp8" - } - }, - "219": { - "container": "webm", - "dash/video": true, - "video": { - "height": 480, - "encoding": "vp8" - } - }, - "242": { - "container": "webm", - "dash/video": true, - "video": { - "height": 240, - "encoding": "vp9" - } - }, - "243": { - "container": "webm", - "dash/video": true, - "video": { - "height": 360, - "encoding": "vp9" - } - }, - "244": { - "container": "webm", - "dash/video": true, - "video": { - "height": 480, - "encoding": "vp9" - } - }, - "247": { - "container": "webm", - "dash/video": true, - "video": { - "height": 720, - "encoding": "vp9" - } - }, - "248": { - "container": "webm", - "dash/video": true, - "video": { - "height": 1080, - "encoding": "vp9" - } - }, - "264": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 1440, - "encoding": "h.264" - } - }, - "266": { - "container": "mp4", - "dash/video": true, - "video": { - "height": 2160, - "encoding": "h.264" - } - }, - "271": { - "container": "webm", - "dash/video": true, - "video": { - "height": 1440, - "encoding": "vp9" - } - }, - "272": { - "container": "webm", - "dash/video": true, - "video": { - "height": 2160, - "encoding": "vp9" - } - }, - "278": { - "container": "webm", - "dash/video": true, - "video": { - "height": 144, - "encoding": "vp9" - } - }, - "298": { - "container": "mp4", - "dash/video": true, - "fps": 60, - "video": { - "height": 720, - "encoding": "h.264" - } - }, - "299": { - "container": "mp4", - "dash/video": true, - "fps": 60, - "video": { - "height": 1080, - "encoding": "h.264" - } - }, - "302": { - "container": "webm", - "dash/video": true, - "fps": 60, - "video": { - "height": 720, - "encoding": "vp9" - } - }, - "303": { - "container": "webm", - "dash/video": true, - "fps": 60, - "video": { - "height": 1080, - "encoding": "vp9" - } - }, - "308": { - "container": "webm", - "dash/video": true, - "fps": 60, - "video": { - "height": 1440, - "encoding": "vp9" - } - }, - "313": { - "container": "webm", - "dash/video": true, - "video": { - "height": 2160, - "encoding": "vp9" - } - }, - "315": { - "container": "webm", - "dash/video": true, - "fps": 60, - "video": { - "height": 2160, - "encoding": "vp9" - } - }, - "330": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 144, - "encoding": "vp9.2" - } - }, - "331": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 240, - "encoding": "vp9.2" - } - }, - "332": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 360, - "encoding": "vp9.2" - } - }, - "333": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 480, - "encoding": "vp9.2" - } - }, - "334": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 720, - "encoding": "vp9.2" - } - }, - "335": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 1080, - "encoding": "vp9.2" - } - }, - "336": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 1440, - "encoding": "vp9.2" - } - }, - "337": { - "container": "webm", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 2160, - "encoding": "vp9.2" - } - }, - "400": { - "container": "mp4", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 1440, - "encoding": "av1" - } - }, - "401": { - "container": "mp4", - "dash/video": true, - "fps": 60, - "hdr": true, - "video": { - "height": 2160, - "encoding": "av1" - } - }, - "394": { - "container": "mp4", - "dash/video": true, - "fps": 30, - "video": { - "height": 144, - "encoding": "av1" - } - }, - "395": { - "container": "mp4", - "dash/video": true, - "fps": 30, - "video": { - "height": 240, - "encoding": "av1" - } - }, - "396": { - "container": "mp4", - "dash/video": true, - "fps": 30, - "video": { - "height": 360, - "encoding": "av1" - } - }, - "397": { - "container": "mp4", - "dash/video": true, - "fps": 30, - "video": { - "height": 480, - "encoding": "av1" - } - }, - "398": { - "container": "mp4", - "dash/video": true, - "fps": 30, - "video": { - "height": 720, - "encoding": "av1" - } - }, - "399": { - "container": "mp4", - "dash/video": true, - "fps": 30, - "video": { - "height": 1080, - "encoding": "av1" - } - }, - "139": { - "container": "mp4", - "sort": [ - 48, - 0 - ], - "title": "aac@48", - "dash/audio": true, - "audio": { - "bitrate": 48, - "encoding": "aac" - } - }, - "140": { - "container": "mp4", - "sort": [ - 129, - 0 - ], - "title": "aac@128", - "dash/audio": true, - "audio": { - "bitrate": 128, - "encoding": "aac" - } - }, - "141": { - "container": "mp4", - "sort": [ - 143, - 0 - ], - "title": "aac@256", - "dash/audio": true, - "audio": { - "bitrate": 256, - "encoding": "aac" - } - }, - "256": { - "container": "mp4", - "title": "aac/itag 256", - "dash/audio": true, - "unsupported": true, - "audio": { - "bitrate": 0, - "encoding": "aac" - } - }, - "258": { - "container": "mp4", - "title": "aac/itag 258", - "dash/audio": true, - "unsupported": true, - "audio": { - "bitrate": 0, - "encoding": "aac" - } - }, - "325": { - "container": "mp4", - "title": "dtse/itag 325", - "dash/audio": true, - "unsupported": true, - "audio": { - "bitrate": 0, - "encoding": "aac" - } - }, - "328": { - "container": "mp4", - "title": "ec-3/itag 328", - "dash/audio": true, - "unsupported": true, - "audio": { - "bitrate": 0, - "encoding": "aac" - } - }, - "171": { - "container": "webm", - "sort": [ - 128, - 0 - ], - "title": "vorbis@128", - "dash/audio": true, - "audio": { - "bitrate": 128, - "encoding": "vorbis" - } - }, - "172": { - "container": "webm", - "sort": [ - 142, - 0 - ], - "title": "vorbis@192", - "dash/audio": true, - "audio": { - "bitrate": 192, - "encoding": "vorbis" - } - }, - "249": { - "container": "webm", - "sort": [ - 50, - 0 - ], - "title": "opus@50", - "dash/audio": true, - "audio": { - "bitrate": 50, - "encoding": "opus" - } - }, - "250": { - "container": "webm", - "sort": [ - 70, - 0 - ], - "title": "opus@70", - "dash/audio": true, - "audio": { - "bitrate": 70, - "encoding": "opus" - } - }, - "251": { - "container": "webm", - "sort": [ - 141, - 0 - ], - "title": "opus@160", - "dash/audio": true, - "audio": { - "bitrate": 160, - "encoding": "opus" - } - }, - "9997": { - "container": "mpd", - "sort": [ - -1, - 0 - ], - "title": "DASH Audio", - "dash/audio": true, - "audio": { - "bitrate": 0, - "encoding": "" - } - }, - "9998": { - "container": "mpd", - "Live": true, - "sort": [ - 1080, - 1 - ], - "title": "Live DASH", - "dash/audio": true, - "dash/video": true, - "audio": { - "bitrate": 0, - "encoding": "" - }, - "video": { - "height": 0, - "encoding": "" - } - }, - "9999": { - "container": "mpd", - "sort": [ - 1080, - 1 - ], - "title": "DASH", - "dash/audio": true, - "dash/video": true, - "audio": { - "bitrate": 0, - "encoding": "" - }, - "video": { - "height": 0, - "encoding": "" - } - } -} diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/mpeg_dash.py b/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/mpeg_dash.py deleted file mode 100644 index ae360f5db..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/mpeg_dash.py +++ /dev/null @@ -1,596 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -import json -import operator -import re -from copy import deepcopy -from html import escape -from urllib.parse import parse_qsl -from urllib.parse import unquote - -import xbmcaddon # pylint: disable=import-error -import xbmcgui # pylint: disable=import-error -import xbmcvfs # pylint: disable=import-error - -from ...utils.logger import Log -from .quality import Quality - -LOG = Log('usher', __file__) - - -class ManifestGenerator: - path = xbmcvfs.translatePath('special://temp/script.module.tubed.api/') - - def __init__(self, itags, license_data=None): - self.addon = xbmcaddon.Addon('script.module.tubed.api') - - self._itags = itags - self._discarded = [] - if license_data is None: - license_data = {} - self.license_data = license_data - - self.component_logging = self.addon.getSettingBool('log.manifest.generator') - - @property - def itags(self): - return self._itags - - @property - def discarded(self): - return self._discarded - - @discarded.setter - def discarded(self, value): - self._discarded.append(value) - - def _make_dirs(self): - if not xbmcvfs.exists(self.path): - _ = xbmcvfs.mkdirs(self.path) - - return xbmcvfs.exists(self.path) - - def discard_audio(self, fmt, mime_type, itag, stream, reason='unsupported'): - discarded = { - 'audio': { - 'itag': str(itag), - 'mime': str(mime_type), - 'codec': str(stream['codec']), - 'bandwidth': int(stream['bandwidth']) - }, - 'reason': reason - } - - if fmt: - bitrate = int(fmt.get('audio', {}).get('bitrate', 0)) - if bitrate > 0: - discarded['audio']['bitrate'] = bitrate - - self.discarded = discarded - - def discard_video(self, mime_type, itag, stream, reason='unsupported'): - discarded = { - 'video': { - 'itag': str(itag), - 'width': str(stream['width']), - 'height': str(stream['height']), - 'fps': str(stream['frameRate']), - 'codec': str(stream['codec']), - 'mime': str(mime_type), - 'bandwidth': int(stream['bandwidth']) - }, - 'reason': reason - } - - if stream.get('quality_label'): - discarded['video']['quality_label'] = str(stream['quality_label']) - - self.discarded = discarded - - def _filter_qualities(self, stream_data, container, quality_object): - data = deepcopy(stream_data) - - height_to_width_map = { - 4320: 7680, - 2160: 3840, - 1440: 2560, - 1080: 1920, - 720: 1280, - 480: 854, - 426: 240, - } - - rng = 1 if container == 'mp4' else 2 - - for idx in range(rng): - mime_mp4 = 'video/mp4' - mime_webm = 'video/webm' - if container == 'mp4' or (container == 'webm' and idx == 1): - discard_mime = mime_webm - try: - discarded_mime_streams = data[mime_webm] - except KeyError: - discarded_mime_streams = [] - - selected_mime = mime_mp4 - streams = deepcopy(data[mime_mp4]) - elif container == 'webm': - discard_mime = mime_mp4 - try: - discarded_mime_streams = data[mime_mp4] - except KeyError: - discarded_mime_streams = [] - - selected_mime = mime_webm - streams = deepcopy(data[mime_webm]) - - if not streams: - discard_mime = mime_webm - try: - discarded_mime_streams = data[mime_webm] - except KeyError: - discarded_mime_streams = [] - - selected_mime = mime_mp4 - streams = deepcopy(data[mime_mp4]) - else: - return data - - thirty_fps_streams = [streams[itag] for itag in list(streams.keys()) - if streams[itag].get('fps') <= 30] - - sixty_fps_streams = [streams[itag] for itag in list(streams.keys()) - if streams[itag].get('fps') > 30] - - fps_streams = sixty_fps_streams - if (not quality_object.limit_30fps and not sixty_fps_streams) or \ - quality_object.limit_30fps: - fps_streams = thirty_fps_streams - - quality_streams = [] - - for quality in quality_object.qualities: - # find all streams with matching width - matches = [stream for stream in fps_streams - if int(stream.get('width', 0)) == height_to_width_map.get(quality, -1)] - - if matches: - quality_streams.extend(matches) - continue - - if not quality_streams: - continue - - quality_streams.sort(key=operator.itemgetter('bandwidth'), reverse=True) - quality_streams.sort(key=operator.itemgetter('width'), reverse=True) - - selected_stream = quality_streams[0] - selected_itag = selected_stream.get('id', -1) - - for itag in list(streams.keys()): - # discard all streams except the best match - if itag != selected_itag: - self.discard_video(selected_mime, itag, streams[itag], 'quality') - del data[selected_mime][itag] - - if discarded_mime_streams: - # discard streams with unwanted mime type - for itag in list(discarded_mime_streams.keys()): - self.discard_video(discard_mime, itag, - discarded_mime_streams[itag], 'mime type') - del data[discard_mime][itag] - - break - - return data - - def _stream_data(self, formats): - data = {} - for item in formats: - stream_map = item - - stream_map.update(dict(parse_qsl(item.get('signatureCipher', item.get('cipher', ''))))) - stream_map['itag'] = str(stream_map.get('itag')) - - mime_type = stream_map.get('mimeType') - mime_type = unquote(mime_type).split(';') - - key = mime_type[0] - itag = stream_map.get('itag') - - if key not in data: - data[key] = {} - data[key][itag] = {} - - codec = str(mime_type[1][1:]) - data[key][itag]['codec'] = codec - - match = re.search('codecs="(?P[^"]+)"', codec) - if match: - data[key][itag]['codec'] = match.group('codec') - - data[key][itag]['id'] = itag - - data[key][itag]['width'] = stream_map.get('width') - data[key][itag]['height'] = stream_map.get('height') - - data[key][itag]['quality_label'] = str(stream_map.get('qualityLabel')) - - data[key][itag]['bandwidth'] = stream_map.get('bitrate', 0) - - # map frame rates to a more common representation to - # lessen the chance of double refresh changes sometimes - # 30 fps is 30 fps, more commonly it is 29.97 fps (same for all mapped frame rates) - frame_rate = None - fps_scale_map = { - 24: 1001, - 30: 1001, - 60: 1001 - } - - if 'fps' in stream_map: - fps = int(stream_map.get('fps')) - data[key][itag]['fps'] = fps - scale = fps_scale_map.get(fps, 1000) - frame_rate = '%d/%d' % (fps * 1000, scale) - - data[key][itag]['frameRate'] = frame_rate - - url = unquote(stream_map.get('url')) - - signature_parameter = '&signature=' - if 'sp' in stream_map: - signature_parameter = '&%s=' % stream_map['sp'] - - if 'sig' in stream_map: - url = ''.join([url, signature_parameter, stream_map['sig']]) - - url = url.replace("&", "&").replace('"', """) - url = url.replace("<", "<").replace(">", ">") - - data[key][itag]['baseUrl'] = url - - data[key][itag]['indexRange'] = '0-0' - data[key][itag]['initRange'] = '0-0' - - if 'indexRange' in stream_map and 'initRange' in stream_map: - data[key][itag]['indexRange'] = \ - '-'.join([stream_map.get('indexRange').get('start'), - stream_map.get('indexRange').get('end')]) - - data[key][itag]['init'] = \ - '-'.join([stream_map.get('initRange').get('start'), - stream_map.get('initRange').get('end')]) - - if ('indexRange' not in stream_map or - 'initRange' not in stream_map or - data[key][itag].get('indexRange') == '0-0' and - data[key][itag].get('initRange') == '0-0'): - - if key.startswith('video'): - self.discard_video(key, itag, data[key][itag], 'no init or index') - - else: - stream_format = self.itags.get(itag, {}) - self.discard_audio(stream_format, key, itag, - data[key][itag], 'no init or index') - - del data[key][itag] - - return data - - def _filter_av1(self, data): - data = deepcopy(data) - payload = {} - - for itag in data.keys(): - if data[itag]['codec'].lower().startswith(('av01', 'av1')): - continue - - payload[itag] = data[itag] - - discarded = [data[itag] for itag in (set(data) - set(payload)) if itag in data] - - for discard in discarded: - self.discard_video('video/mp4', discard['id'], - data[discard['id']], 'av1 unsupported') - - return payload - - def _filter_hdr(self, data, hdr=False): - data = deepcopy(data) - webm = {} - - if hdr and any(itag for itag in data.keys() - if 'vp9.2' in data[itag]['codec']): - # when hdr enabled and available replace vp9 streams with vp9.2 (hdr) - for itag in data.keys(): - if 'vp9.2' in data[itag]['codec']: - webm[itag] = data[itag] - - discarded = [data[itag] for itag in (set(data) - set(webm)) if itag in data] - - for discard in discarded: - self.discard_video('video/webm', discard['id'], - data[discard['id']], 'replaced by hdr') - - elif not hdr: - # when hdr disabled and remove vp9.2 (hdr) streams - for itag in data.keys(): - if 'vp9.2' in data[itag]['codec']: - continue - - webm[itag] = data[itag] - - discarded = [data[itag] for itag in (set(data) - set(webm)) if itag in data] - - for discard in discarded: - self.discard_video('video/webm', discard['id'], - data[discard['id']], 'hdr disabled') - - return webm - - @staticmethod - def _stream_info_template(): - return { - 'video': { - 'height': '0', - 'fps': '0', - 'codec': '', - 'mime': '', - 'quality_label': '', - 'bandwidth': 0 - }, - 'audio': { - 'bitrate': '0', - 'codec': '', - 'mime': '', - 'bandwidth': 0 - } - } - - def generate(self, video_id, formats, duration, quality_object=None): # pylint: disable=too-many-branches,too-many-statements,too-many-locals - LOG.debug('Generating MPEG-DASH manifest for %s' % video_id) - - if self.component_logging: - LOG.debug('Stream information available for %s MPEG-DASH manifest:\n%s' % - (video_id, json.dumps(formats, indent=4))) - - if not self._make_dirs(): - return None - - if not quality_object: - quality_object = Quality('mp4') - - has_video_stream = False - - stream_info = self._stream_info_template() - data = self._stream_data(formats) - - if not data.get('video/mp4') and not data.get('video/webm'): - return None, None - - default_mime_type = 'mp4' - supported_mime_types = [] - - if data.get('video/mp4'): - supported_mime_types.append('video/mp4') - - if data.get('audio/mp4'): - supported_mime_types.append('audio/mp4') - - if any(mime for mime in data if mime == 'video/webm') and data.get('video/webm'): - supported_mime_types.append('video/webm') - - if ('video/webm' in supported_mime_types and - ((isinstance(quality_object.quality, str) and quality_object.quality == 'webm') or - (isinstance(quality_object.quality, int) and quality_object.quality > 1080) or - quality_object.hdr)): - default_mime_type = 'webm' - - av1_filtered = self._filter_av1(data.get('video/mp4', {})) - if av1_filtered: - data['video/mp4'] = av1_filtered - - hdr_filtered = self._filter_hdr(data.get('video/webm', {}), hdr=quality_object.hdr) - if hdr_filtered: - data['video/webm'] = hdr_filtered - - if isinstance(quality_object.quality, int) and isinstance(quality_object.qualities, list): - data = self._filter_qualities(data, default_mime_type, quality_object) - - mpd_list = ['\n' - '\n', - '\t\n'] - - adaptation_set_idx = 0 - for key in data: # pylint: disable=too-many-nested-blocks - if "_" in key: - mime, lang = key.split("_") - else: - mime = key - lang = None - - if mime in supported_mime_types: - default = False - if mime.endswith(default_mime_type): - default = True - - mpd_list.append(''.join(['\t\t\n'])) - - license_url = self.license_data.get('license_url') - if license_url: - mpd_list.append(''.join(['\t\t\t\n', - '\t\t\t\t', - escape(license_url), '\n', - '\t\t\t\n'])) - - mpd_list.append('\t\t\t\n') - - for itag in data[key]: - stream_format = self.itags.get(itag, {}) - if 'audio' in mime: - - audio_codec = data[key][itag]['codec'] - if audio_codec.lower() == 'opus': - self.discard_audio(stream_format, mime, itag, data[key][itag]) - continue - - if audio_codec.lower() == 'vorbis': - self.discard_audio(stream_format, mime, itag, data[key][itag]) - continue - - if (int(data[key][itag]['bandwidth']) > - int(stream_info['audio']['bandwidth'])): - - stream_info['audio']['mime'] = str(mime) - if stream_format: - - bitrate = int(stream_format.get('audio', {}).get('bitrate', 0)) - if bitrate > 0: - stream_info['audio']['bitrate'] = str(bitrate) - - stream_info['audio']['codec'] = \ - stream_format.get('audio', {}).get('encoding') - - if not stream_info['audio'].get('codec'): - stream_info['audio']['codec'] = audio_codec - - stream_info['audio']['bandwidth'] = int(data[key][itag]['bandwidth']) - - mpd_list.append(''.join(['\t\t\t\n'])) - - mpd_list.append('\t\t\t\t\n') - - else: - video_codec = data[key][itag]['codec'] - - if video_codec.lower() == 'vp9.2' and not quality_object.hdr: - self.discard_video(mime, itag, data[key][itag], 'hdr not selected') - continue - - if video_codec.lower().startswith(('av01', 'av1')) and \ - not quality_object.av1: - self.discard_video(mime, itag, data[key][itag], 'av1 not selected') - continue - - has_video_stream = True - if default: - if (int(data[key][itag]['bandwidth']) > - int(stream_info['video']['bandwidth'])): - stream_info['video']['height'] = str(data[key][itag]['height']) - stream_info['video']['fps'] = str(data[key][itag]['frameRate']) - stream_info['video']['mime'] = str(mime) - stream_info['video']['codec'] = video_codec - stream_info['video']['bandwidth'] = \ - int(data[key][itag]['bandwidth']) - - if data[key][itag].get('quality_label'): - stream_info['video']['quality_label'] = \ - str(data[key][itag]['quality_label']) - - if stream_format: - stream_info['video']['codec'] = \ - stream_format.get('video', {}).get('encoding') - - if not stream_info['video'].get('codec'): - stream_info['video']['codec'] = video_codec - - video_codec = data[key][itag]['codec'] - mpd_list.append(''.join(['\t\t\t\n'])) - - mpd_list.append(''.join(['\t\t\t\t', - data[key][itag]['baseUrl'], - '\n'])) - - mpd_list.append(''.join(['\t\t\t\t\n', '\t\t\t\t\t\t\n', - '\t\t\t\t\n'])) - - mpd_list.append('\t\t\t\n') - mpd_list.append('\t\t\n') - - adaptation_set_idx = adaptation_set_idx + 1 - - else: - for i in data[key]: - stream_format = self.itags.get(i, {}) - if 'audio' in mime: - self.discard_audio(stream_format, mime, i, data[key][i]) - - else: - self.discard_video(mime, i, data[key][i]) - - mpd_list.append('\t\n\n') - manifest_contents = ''.join(mpd_list) - - if self.discarded: - self._discarded = sorted( - self.discarded, - key=lambda k: (k.get('reason'), k.get('audio', k.get('video', {}))['bandwidth']), - reverse=True - ) - - if not has_video_stream: - pass - - filename = '{path}{video_id}.mpd'.format(path=self.path, video_id=video_id) - with open(filename, 'wb') as file_handle: - _ = file_handle.write(bytes(manifest_contents, encoding='utf-8')) - - license_url = self.license_data.get('license_url', '') - license_token = self.license_data.get('license_token', '') - if license_url and license_token: - xbmcgui.Window(10000).setProperty('tubed-api-license_url', license_url) - xbmcgui.Window(10000).setProperty('tubed-api-license_token', license_token) - - port = self.addon.getSettingInt('httpd.port') or 52520 - proxy_url = 'http://127.0.0.1:{port}/{video_id}.mpd'.format(port=port, video_id=video_id) - - if self.component_logging: - LOG.debug('Stream information discard for %s MPEG-DASH manifest:\n%s' % - (video_id, json.dumps(self.discarded, indent=4))) - LOG.debug('Stream information used for %s MPEG-DASH manifest:\n%s' % - (video_id, json.dumps(stream_info, indent=4))) - - LOG.debug('Finished generating MPEG-DASH manifest for %s @ %s' % (video_id, proxy_url)) - - return proxy_url, stream_info diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/quality.py b/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/quality.py deleted file mode 100644 index 54d7db48b..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/quality.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - - -class Quality: - MAP = { - 0: 240, - 1: 360, - 2: 480, - 3: 720, - 4: 1080, - 5: 1440, - 6: 2160, - 7: 4320, - 8: 'mp4', - 9: 'webm' - } - - def __init__(self, height, limit_30fps=True, hdr=False, av1=False): - - self._index = self._index_of_height(height) - self._limit_30fps = limit_30fps - self._hdr = hdr - if hdr: - self._limit_30fps = False - - self._av1 = av1 - - @property - def map(self): - return self.MAP - - @property - def quality(self): - return self.map.get(self._index, 'mp4') - - @property - def qualities(self): - if not isinstance(self.quality, int): - return self.quality - - qualities = sorted([quality for quality in self.map.values() - if isinstance(quality, int) and quality <= self.quality], - reverse=True) - - return qualities - - @property - def limit_30fps(self): - if self.hdr: - return False - return self._limit_30fps - - @property - def hdr(self): - return self._hdr - - @property - def av1(self): - return self._av1 - - def _index_of_height(self, height): - for index, quality in self.MAP.items(): - if height == quality: - return index - - return 8 diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/subtitles.py b/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/subtitles.py deleted file mode 100644 index d38dfa3b7..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/subtitles.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2017-2020 plugin.video.youtube - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only for more information. -""" - -from urllib.parse import parse_qs -from urllib.parse import urlencode -from urllib.parse import urlsplit -from urllib.parse import urlunsplit - - -class Subtitles: - def __init__(self, video_id, captions): - self.video_id = video_id - - self.caption_track = {} - - renderer = captions.get('playerCaptionsTracklistRenderer', {}) - - self.caption_tracks = renderer.get('captionTracks', []) - self.translation_languages = renderer.get('translationLanguages', []) - - default_audio = renderer.get('defaultAudioTrackIndex') - if default_audio: - audio_tracks = renderer.get('audioTracks', []) - - try: - audio_track = audio_tracks[default_audio] - except: # pylint: disable=bare-except - audio_track = None - - if audio_track: - default_caption = audio_track.get('defaultCaptionTrackIndex') - - if not default_caption: - default_caption = audio_track.get('captionTrackIndices') - if default_caption and isinstance(default_caption, list): - default_caption = default_caption[0] - - if default_caption and default_caption in self.caption_tracks: - self.caption_track = self.caption_tracks[default_caption] - - def retrieve(self): - list_of_subs = [] - - all_captions = self.translation_languages + self.caption_tracks - for language in all_captions: - subtitle = self._get(language=language.get('languageCode')) - if subtitle: - list_of_subs.append(subtitle) - - return list(set(list_of_subs)) - - def _get(self, language='en'): - caption = None - - for track in self.caption_tracks: - if language == track.get('languageCode'): - if track.get('kind') == 'asr': - if not caption: - caption = track - continue - - caption = track - break - - has_translation = any(lang for lang in self.translation_languages - if lang.get('languageCode') == language) - - if not has_translation and not caption: - return None - - subtitle_url = None - if not caption and has_translation: - base_url = self.caption_track.get('baseUrl') - if base_url: - subtitle_url = self.set_query_param(base_url, 'tlang', language) - - elif caption: - base_url = caption.get('baseUrl') - if base_url: - subtitle_url = base_url - - if subtitle_url: - subtitle_url = self.set_query_param(subtitle_url, 'type', 'track') - subtitle_url = self.set_query_param(subtitle_url, 'fmt', 'vtt') - return (caption.get('languageCode'), - self._get_language_name(caption), - caption.get('kind'), - subtitle_url) - - return None - - @staticmethod - def _get_language_name(track): - key = 'languageName' if 'languageName' in track else 'name' - - lang_name = track.get(key, {}).get('simpleText') - if lang_name: - return lang_name - - if not lang_name: - track_name = track.get(key, {}).get('runs', [{}]) - - if isinstance(track_name, list) and len(track_name) >= 1: - return track_name[0].get('text') - - return '' - - @staticmethod - def set_query_param(url, name, value): - scheme, netloc, path, query_string, fragment = urlsplit(url) - query_params = parse_qs(query_string) - - query_params[name] = [value] - new_query_string = urlencode(query_params, doseq=True) - if isinstance(scheme, bytes): - new_query_string = new_query_string.encode('utf-8') - - return urlunsplit((scheme, netloc, path, new_query_string, fragment)) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/video_info.py b/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/video_info.py deleted file mode 100644 index 6a1972a2a..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/lib/video_info.py +++ /dev/null @@ -1,390 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - Copyright (C) 2014-2016 bromix (plugin.video.youtube) - Copyright (C) 2016-2020 plugin.video.youtube - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -import json -import os -import random -import traceback -from base64 import b64decode -from copy import deepcopy -from urllib.parse import quote - -import requests -import xbmcaddon # pylint: disable=import-error -import xbmcvfs # pylint: disable=import-error - -from ...constants.http import MOBILE_HEADERS -from ...exceptions import ContentNoResponse -from ...exceptions import ContentRestricted -from ...utils.logger import Log -from .mpeg_dash import ManifestGenerator -from .quality import Quality -from .subtitles import Subtitles - -LOG = Log('usher', __file__) - - -class VideoInfo: - - def __init__(self, language='en-US', region='US'): - from ... import ACCESS_TOKEN_TV # pylint: disable=import-outside-toplevel - from ... import API_KEY_TV # pylint: disable=import-outside-toplevel - - self._access_token_tv = ACCESS_TOKEN_TV - self._api_key_tv = b64decode(API_KEY_TV).decode('utf-8') - self._language = language - self._region = region - self._itags = {} - - self.addon = xbmcaddon.Addon('script.module.tubed.api') - - @property - def language(self): - return self._language - - @property - def region(self): - return self._region - - @property - def headers(self): - return deepcopy(MOBILE_HEADERS.copy()) - - @property - def itags(self): - if not self._itags: - self._load_itags() - return self._itags - - def _load_itags(self): - filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'itags.json') - with xbmcvfs.File(filename, 'r') as itag_file: - self._itags = json.load(itag_file) - - @staticmethod - def generate_cpn(): - # https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L1381 - # LICENSE: The Unlicense - # cpn generation algorithm is reverse engineered from base.js. - # In fact it works even with dummy cpn. - cpn_alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' - cpn = ''.join((cpn_alphabet[random.randint(0, 256) & 63] for _ in range(0, 16))) - return cpn - - @staticmethod - def make_curl_headers(headers, cookies=None): - output = '' - if cookies: - output += 'Cookie={all_cookies}'.format( - all_cookies=quote( - '; '.join('{0}={1}'.format(c.name, c.value) for c in cookies) - ) - ) - output += '&' - # Headers to be used in function 'to_play_item' of 'xbmc_items.py'. - output += '&'.join('{0}={1}'.format(key, quote(headers[key])) - for key in headers) - return output - - @staticmethod - def image_map(): - return [ - { - 'from': 'iurlhq', - 'to': 'high', - 'image': 'hqdefault.jpg' - }, - { - 'from': 'iurlmq', - 'to': 'medium', - 'image': 'mqdefault.jpg' - }, - { - 'from': 'iurlsd', - 'to': 'standard', - 'image': 'sddefault.jpg' - }, - { - 'from': 'thumbnail_url', - 'to': 'default', - 'image': 'default.jpg' - } - ] - - @staticmethod - def playability(status): - if ((status.get('fallback', True) and status.get('status', 'ok').lower() == 'ok') or - status.get('desktopLegacyAgeGateReason', 1) == 1): - return { - 'playable': True, - 'reason': '' - } - - if status.get('status') == 'LIVE_STREAM_OFFLINE': - reason = status.get('reason') - if not reason: - streamability = status.get('liveStreamability', {}) - renderer = streamability.get('liveStreamabilityRenderer', {}) - slate = renderer.get('offlineSlate', {}) - offline_slate_renderer = slate.get('liveStreamOfflineSlateRenderer', {}) - main_text = offline_slate_renderer.get('mainText', {}) - text_runs = main_text.get('runs', [{}]) - - reason_text = [] - for text in text_runs: - reason_text.append(text.get('text', '')) - - if reason_text: - reason = ''.join(reason_text) - else: - reason = status.get('reason') - - if 'errorScreen' in status and 'playerErrorMessageRenderer' in status['errorScreen']: - renderer = status['errorScreen']['playerErrorMessageRenderer'] - - descript_reason = renderer.get('subreason', {}).get('simpleText') - general_reason = renderer.get('reason', {}).get('simpleText') - - if descript_reason: - reason = descript_reason - elif general_reason: - reason = general_reason - - if not reason: - reason = 'UNKNOWN' - - try: - reason = reason.encode('raw_unicode_escape').decode('utf-8') - except: # pylint: disable=bare-except - pass - - LOG.error('Video is unplayable: %s' % reason) - return { - 'playable': False, - 'reason': reason - } - - def get_video(self, video_id, quality=None): # pylint: disable=too-many-locals, too-many-branches, too-many-statements - LOG.debug('Retrieving video information for %s' % video_id) - - headers = self.headers.copy() - - params = None - if self._access_token_tv: - headers['Authorization'] = 'Bearer %s' % self._access_token_tv - else: - params = { - 'key': self._api_key_tv - } - video_info_url = 'https://youtubei.googleapis.com/youtubei/v1/player' - # payload = {'videoId': video_id, - # 'context': {'client': {'clientVersion': '1.20210909.07.00', 'gl': self.region, - # 'clientName': 'WEB_CREATOR', 'hl': self.language}}} - - # payload = {'videoId': video_id, - # 'context': {'client': {'clientVersion': '16.05', 'gl': self.region, - # 'clientName': 'ANDROID', 'clientScreen': 'EMBED', - # 'hl': self.language}}} - - payload = { - 'videoId': video_id, - 'context': { - 'client': { - 'clientVersion': '16.05', - 'gl': self.region, - 'clientName': 'ANDROID', - 'hl': self.language - } - } - } - - player_response = {} - for attempt in range(2): - try: - response_payload = requests.post(video_info_url, params=params, - json=payload, headers=headers, - cookies=None, allow_redirects=True, - timeout=60) - response_payload.raise_for_status() - player_response = response_payload.json() - if player_response.get('playabilityStatus', {}).get('status', 'OK') == \ - 'AGE_CHECK_REQUIRED' and attempt == 0: - payload['context']['client']['clientScreen'] = 'EMBED' - continue - except: - error_message = 'Failed to get player response for video_id "%s"' % video_id - LOG.error(error_message + '\n' + traceback.format_exc()) - - raise ContentNoResponse(error_message) # pylint: disable=raise-missing-from - - # Make a set of URL-quoted headers to be sent to Kodi when requesting - # the stream during playback. The YT player doesn't seem to use any - # cookies when doing that, so for now cookies are ignored. - # curl_headers = self.make_curl_headers(headers, cookies) - curl_headers = self.make_curl_headers(headers, cookies=None) - - playability_status = player_response.get('playabilityStatus', {}) - - playback_tracking = player_response.get('playbackTracking', {}) - captions = player_response.get('captions', {}) - video_details = player_response.get('videoDetails', {}) - is_live_content = video_details.get('isLiveContent') is True - streaming_data = player_response.get('streamingData', {}) - - live_url = streaming_data.get('hlsManifestUrl', '') - is_live = is_live_content and live_url - - metadata = { - 'video': {}, - 'channel': {}, - 'images': {}, - 'subtitles': [] - } - - metadata['video']['id'] = video_details.get('videoId', video_id) - metadata['video']['title'] = video_details.get('title', '') - metadata['channel']['author'] = video_details.get('author', '') - metadata['channel']['id'] = video_details.get('channelId', '') - - for image_meta in self.image_map(): - image_url = 'https://i.ytimg.com/vi/{video_id}/{image}' \ - .format(video_id=video_id, image=image_meta['image']) - - if is_live: - image_url = image_url.replace('.jpg', '_live.jpg') - - metadata['images'][image_meta['to']] = image_url - - microformat = player_response.get('microformat', {}).get('playerMicroformatRenderer', {}) - metadata['video']['status'] = { - 'unlisted': microformat.get('isUnlisted', False), - 'private': video_details.get('isPrivate', False), - 'crawlable': video_details.get('isCrawlable', False), - 'family_safe': microformat.get('isFamilySafe', False), - 'live': is_live, - } - - status = self.playability(playability_status) - if not status['playable']: - raise ContentRestricted({ - 'error': 'content_restricted', - 'error_description': status['reason'], - 'code': '403' - }) - - metadata['subtitles'] = Subtitles(video_id, captions).retrieve() - - report = { - 'playback_url': '', - 'watchtime_url': '' - } - - playback_url = playback_tracking.get('videostatsPlaybackUrl', {}).get('baseUrl', '') - watchtime_url = playback_tracking.get('videostatsWatchtimeUrl', {}).get('baseUrl', '') - - if playback_url and playback_url.startswith('http'): - report['playback_url'] = ''.join([ - playback_url, - '&ver=2&fs=0&volume=100&muted=0', - '&cpn={cpn}'.format(cpn=self.generate_cpn()) - ]) - - if watchtime_url and watchtime_url.startswith('http'): - report['watchtime_url'] = ''.join([ - watchtime_url, - '&ver=2&fs=0&volume=100&muted=0', - '&cpn={cpn}'.format(cpn=self.generate_cpn()), - '&st={st}&et={et}&state={state}' - ]) - - stream_info = {} - - adaptive_formats = streaming_data.get('adaptiveFormats', []) - # standard_formats = streaming_data.get('formats', []) - - mpd_url = streaming_data.get('dashManifestUrl', '') - - license_data = { - 'url': None, - 'proxy': None, - 'token': None - } - - license_infos = streaming_data.get('licenseInfos', []) - for license_info in license_infos: - if license_info.get('drmFamily') == 'WIDEVINE': - license_data['url'] = license_info.get('url', '') - if license_data['url']: - license_data['proxy'] = 'http://127.0.0.1:%s/widevine||R{SSM}|' % self.addon \ - .getSettingInt('httpd.port') or 52520 - license_data['token'] = self._access_token_tv - break - - if not is_live: - if not quality: - quality = Quality('mp4') - - mpd_url, stream_info = \ - ManifestGenerator(self.itags, license_data).generate( - video_id, - adaptive_formats, - video_details.get('lengthSeconds', '0'), - quality - ) - - video_stream = { - 'url': mpd_url, - 'metadata': metadata, - 'headers': curl_headers, - 'license': license_data, - 'report': report - } - - if is_live: - video_stream['url'] = '&'.join([video_stream['url'], 'start_seq=$START_NUMBER$']) - video_stream.update(self.itags.get('9998')) - return video_stream - - if not stream_info: - video_stream.update(self.itags.get('9999')) - return video_stream - - has_video = (stream_info['video']['codec'] != '' and - int(stream_info['video']['bandwidth']) > 0) - if has_video: - video_stream.update(self.itags.get('9999')) - video_stream['video']['height'] = stream_info['video']['height'] - video_stream['video']['encoding'] = stream_info['video']['codec'] - else: - video_stream.update(self.itags.get('9997')) - - video_stream['audio']['encoding'] = stream_info['audio']['codec'] - if int(stream_info['audio'].get('bitrate', 0)) > 0: - video_stream['audio']['bitrate'] = int(stream_info['audio'].get('bitrate', 0)) - - if stream_info['video']['quality_label']: - video_stream['title'] = stream_info['video']['quality_label'] - return video_stream - - if has_video: - video_stream['title'] = '%sp%s' % \ - (stream_info['video']['height'], - stream_info['video']['fps']) - return video_stream - - video_stream['title'] = '%s@%s' % \ - (stream_info['audio']['codec'], - str(stream_info['audio'].get('bitrate', 0))) - - LOG.debug('Retrieved video information for %s: %s' % (video_id, video_stream)) - return video_stream diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/usher/resolver.py b/script.module.tubed.api/resources/lib/src/tubed_api/usher/resolver.py deleted file mode 100644 index dba71eaed..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/usher/resolver.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from .lib.quality import Quality -from .lib.video_info import VideoInfo - - -def resolve(video_id, quality=None, language='en-US', region='US'): - if not quality: - quality = Quality('mp4') - elif isinstance(quality, (int, str)): - quality = Quality(quality) - - video_info = VideoInfo(language, region) - video = video_info.get_video(video_id, quality) - - return video diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/utils/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/utils/__init__.py deleted file mode 100644 index af8fcabec..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/utils/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -__all__ = ['logger', 'time'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/utils/logger.py b/script.module.tubed.api/resources/lib/src/tubed_api/utils/logger.py deleted file mode 100644 index 6c47a2e1f..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/utils/logger.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -import logging -import os - -try: - import xbmc -except ImportError: - xbmc = None - -LOGGER_NAME = 'script.module.tubed.api' - - -class Log: - def __init__(self, package='', module='', filename=''): - """ - A simple logger for logging to the Kodi log, Console or a separate Log file - - :param package: name of the calling package if relevant - :type package: str - :param module: name of the calling module if relevant - :type module: str - :param filename: filename and path of log file if logging to file is desired - :type filename: str - """ - self._package = package - - module = os.path.basename(module) - self._module = module.replace('.pyo', '').replace('.pyc', '').replace('.py', '') - - self._filename = filename - - if xbmc: - self._log = xbmc.log - else: - self._create_logger() - - def info(self, message): - """ - Log at `Info` level - :param message: message to log - :type message: [str, bytes] - """ - message = self._decode_message(message) - if xbmc: - message = self._format_message(message) - self._log(message, xbmc.LOGINFO) - else: - self._log.info(message) - - def debug(self, message): - """ - Log at `Debug` level - :param message: message to log - :type message: [str, bytes] - """ - message = self._decode_message(message) - if xbmc: - message = self._format_message(message) - self._log(message, xbmc.LOGDEBUG) - else: - self._log.debug(message) - - def warning(self, message): - """ - Log at `Warning` level - :param message: message to log - :type message: [str, bytes] - """ - message = self._decode_message(message) - if xbmc: - message = self._format_message(message) - self._log(message, xbmc.LOGWARNING) - else: - self._log.debug(message) - - def error(self, message): - """ - Log at `Error` level - :param message: message to log - :type message: [str, bytes] - """ - message = self._decode_message(message) - if xbmc: - message = self._format_message(message) - self._log(message, xbmc.LOGERROR) - else: - self._log.error(message) - - def critical(self, message): - """ - Log at `Critical/Fatal` level - :param message: message to log - :type message: [str, bytes] - """ - message = self._decode_message(message) - if xbmc: - message = self._format_message(message) - self._log(message, xbmc.LOGFATAL) - else: - self._log.critical(message) - - @staticmethod - def _decode_message(message): - """ - If message is `bytes` decode and return - :param message: message to decode - :type message: [str, bytes] - :return: decoded message - :rtype: str - """ - if isinstance(message, bytes): - message = message.decode('utf-8') - - return message - - def _format_message(self, message): - """ - Format the log message for Kodi - :param message: log message to format - :type message: str - :return: log message with log leaders added - :rtype: str - """ - if self._package and not self._module: - return '[%s][%s] %s' % (LOGGER_NAME, self._package, message) - - if not self._package and self._module: - return '[%s][%s] %s' % (LOGGER_NAME, self._module, message) - - if self._package and self._module: - return '[%s][%s][%s] %s' % (LOGGER_NAME, self._package, self._module, message) - - return '[%s] %s' % (LOGGER_NAME, message) - - def _create_logger(self): - """ - Create a python logger - Creates a file based logger if a filename was provided, otherwise use console based logging - """ - self._log = logging.getLogger(LOGGER_NAME) - - self._log.setLevel(logging.DEBUG) - self._log.propagate = False - - formatter = self._get_formatter() - - if not self._filename: - handler = logging.StreamHandler() - - else: - handler = logging.handlers.RotatingFileHandler(self._filename, - encoding='utf-8', mode="w") - handler.setLevel(logging.DEBUG) - handler.setFormatter(formatter) - self._log.addHandler(handler) - - def _get_formatter(self): - """ - Get formatter for python logging - :return: formatter for python logging - :rtype: logging.Formatter - """ - fmt_lead = '%(asctime)s-[%(levelname)s]' - fmt_tail = ' %(message)s' - fmt = '[%(name)s]' - - if self._package and not self._module: - fmt = '[%s]' % self._package - - elif not self._package and self._module: - fmt = '[%s][%s]' % ('%(name)s', self._module) - - elif self._package and self._module: - fmt = '[%s][%s]' % (self._package, self._module) - - return logging.Formatter(fmt.join([fmt_lead, fmt_tail])) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/utils/time.py b/script.module.tubed.api/resources/lib/src/tubed_api/utils/time.py deleted file mode 100644 index 1351f502d..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/utils/time.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -import time -from datetime import datetime - -now = datetime.now - - -def strptime(timestamp, timestamp_format): - import _strptime # pylint: disable=import-outside-toplevel - try: - time.strptime('01 01 2012', '%d %m %Y') - finally: - return time.strptime(timestamp, timestamp_format) # pylint: disable=lost-exception - - -def timestamp_diff(timestamp): - try: - then = datetime(*(strptime(timestamp, '%Y-%m-%d %H:%M:%S.%f')[0:6])) - except ValueError: - then = datetime(*(strptime(timestamp, '%Y-%m-%d %H:%M:%S')[0:6])) - - delta = now() - then - - return delta.total_seconds() diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/__init__.py deleted file mode 100644 index cce945760..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from .api import activities -from .api import captions -from .api import channel_banners -from .api import channel_sections -from .api import channels -from .api import comment_threads -from .api import comments -from .api import guide_categories -from .api import i18n_languages -from .api import i18n_regions -from .api import members -from .api import membership_levels -from .api import playlist_items -from .api import playlists -from .api import search -from .api import subscriptions -from .api import thumbnails -from .api import video_abuse_report_reasons -from .api import video_categories -from .api import videos -from .api import watermarks - -__all__ = ['activities', 'captions', 'channel_banners', 'channel_sections', - 'channels', 'comment_threads', 'comments', 'guide_categories', - 'i18n_languages', 'i18n_regions', 'members', 'membership_levels', - 'playlist_items', 'playlists', 'search', 'subscriptions', - 'thumbnails', 'video_abuse_report_reasons', 'video_categories', 'videos', - 'watermarks'] - -__all__ += ['query', 'request_handler'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/__init__.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/__init__.py deleted file mode 100644 index 0092e6196..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -__all__ = ['activities', 'captions', 'channel_banners', 'channel_sections', - 'channels', 'comment_threads', 'comments', 'guide_categories', - 'i18n_languages', 'i18n_regions', 'members', 'membership_levels', - 'playlist_items', 'playlists', 'search', 'subscriptions', - 'thumbnails', 'video_abuse_report_reasons', 'video_categories', 'videos', - 'watermarks'] diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/activities.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/activities.py deleted file mode 100644 index 48f1d3ce7..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/activities.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/activities/list -@query -def get(parameters=None): - return V3Query('get', 'activities', parameters=parameters) - - -# deprecated -# https://developers.google.com/youtube/v3/docs/activities/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'activities', parameters=parameters, data=data) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/captions.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/captions.py deleted file mode 100644 index 6c07a2091..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/captions.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/captions/list -@query -def get(parameters=None): - return V3Query('get', 'captions', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/captions/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'captions', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/captions/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'captions', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/captions/download -@query -def download(caption_id, parameters=None): - return V3Query('get', 'captions/{id}'.format(id=caption_id), parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/captions/delete -@query -def delete(parameters=None): - return V3Query('delete', 'captions', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channel_banners.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channel_banners.py deleted file mode 100644 index 29d6d47ad..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channel_banners.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/channelBanners/insert -@query -def insert(parameters=None): - return V3Query('post', 'channelBanners', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channel_sections.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channel_sections.py deleted file mode 100644 index 7508ae796..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channel_sections.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/channelSections/list -@query -def get(parameters=None): - return V3Query('get', 'channelSections', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/channelSections/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'channelSections', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/channelSections/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'channelSections', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/channelSections/delete -@query -def delete(parameters=None): - return V3Query('delete', 'channelSections', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channels.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channels.py deleted file mode 100644 index 4df9cbe73..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/channels.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/channels/list -@query -def get(parameters=None): - return V3Query('get', 'channels', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/channels/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'channels', parameters=parameters, data=data) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/comment_threads.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/comment_threads.py deleted file mode 100644 index 3ad42c868..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/comment_threads.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/commentThreads/list -@query -def get(parameters=None, unauthorized=False): - return V3Query('get', 'commentThreads', parameters=parameters, unauthorized=unauthorized) - - -# https://developers.google.com/youtube/v3/docs/commentThreads/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'commentThreads', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/commentThreads/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'commentThreads', parameters=parameters, data=data) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/comments.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/comments.py deleted file mode 100644 index 7e028f6c2..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/comments.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/comments/list -@query -def get(parameters=None, unauthorized=False): - return V3Query('get', 'comments', parameters=parameters, unauthorized=unauthorized) - - -# https://developers.google.com/youtube/v3/docs/comments/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'comments', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/comments/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'comments', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/comments/markAsSpam -@query -def mark_as_spam(parameters=None): - return V3Query('post', 'comments/markAsSpam', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/comments/setModerationStatus -@query -def set_moderation_status(parameters=None): - return V3Query('post', 'comments/setModerationStatus', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/comments/delete -@query -def delete(parameters=None): - return V3Query('delete', 'comments', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/guide_categories.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/guide_categories.py deleted file mode 100644 index 4ab2c9f9e..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/guide_categories.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# deprecated -# https://developers.google.com/youtube/v3/docs/guideCategories/list -@query -def get(parameters=None): - return V3Query('get', 'guideCategories', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/i18n_languages.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/i18n_languages.py deleted file mode 100644 index 81ad08a43..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/i18n_languages.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/i18nLanguages/list -@query -def get(parameters=None): - return V3Query('get', 'i18nLanguages', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/i18n_regions.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/i18n_regions.py deleted file mode 100644 index 4eba78985..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/i18n_regions.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/i18nRegions/list -@query -def get(parameters=None): - return V3Query('get', 'i18nRegions', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/members.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/members.py deleted file mode 100644 index 8fa36cf6d..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/members.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/members/list -@query -def get(parameters=None): - return V3Query('get', 'members', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/membership_levels.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/membership_levels.py deleted file mode 100644 index 686a862fc..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/membership_levels.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/membershipsLevels/list -@query -def get(parameters=None): - return V3Query('get', 'membershipsLevels', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/playlist_items.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/playlist_items.py deleted file mode 100644 index 6fab0106a..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/playlist_items.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/playlistItems/list -@query -def get(parameters=None): - return V3Query('get', 'playlistItems', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/playlistItems/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'playlistItems', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/playlistItems/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'playlistItems', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/playlistItems/delete -@query -def delete(parameters=None): - return V3Query('delete', 'playlistItems', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/playlists.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/playlists.py deleted file mode 100644 index e9dcb2306..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/playlists.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/playlists/list -@query -def get(parameters=None): - return V3Query('get', 'playlists', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/playlists/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'playlists', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/playlists/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'playlists', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/playlists/delete -@query -def delete(parameters=None): - return V3Query('delete', 'playlists', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/search.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/search.py deleted file mode 100644 index f499baca8..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/search.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/search/list -@query -def get(parameters=None): - return V3Query('get', 'search', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/subscriptions.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/subscriptions.py deleted file mode 100644 index 1894e7813..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/subscriptions.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/subscriptions/list -@query -def get(parameters=None): - return V3Query('get', 'subscriptions', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/subscriptions/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'subscriptions', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/subscriptions/delete -@query -def delete(parameters=None): - return V3Query('delete', 'subscriptions', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/thumbnails.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/thumbnails.py deleted file mode 100644 index 3e846e29d..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/thumbnails.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/thumbnails/set -@query -def upload(parameters=None, data=None): - return V3Query('post', 'thumbnails/set', parameters=parameters, data=data) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/video_abuse_report_reasons.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/video_abuse_report_reasons.py deleted file mode 100644 index 8ecadf60f..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/video_abuse_report_reasons.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons/list -@query -def get(parameters=None): - return V3Query('get', 'videoAbuseReportReasons', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/video_categories.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/video_categories.py deleted file mode 100644 index 8934ecfab..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/video_categories.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/videoCategories/list -@query -def get(parameters=None): - return V3Query('get', 'videoCategories', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/videos.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/videos.py deleted file mode 100644 index c4b0839cc..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/videos.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/videos/list -@query -def get(parameters=None): - return V3Query('get', 'videos', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/videos/insert -@query -def insert(parameters=None, data=None): - return V3Query('post', 'videos', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/videos/update -@query -def update(parameters=None, data=None): - return V3Query('put', 'videos', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/videos/rate -@query -def rate(parameters=None): - return V3Query('post', 'videos/rate', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/videos/getRating -@query -def get_rating(parameters=None): - return V3Query('get', 'videos/getRating', parameters=parameters) - - -# https://developers.google.com/youtube/v3/docs/videos/reportAbuse -@query -def report_abuse(parameters=None, data=None): - return V3Query('post', 'videos/reportAbuse', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/videos/delete -@query -def delete(parameters=None): - return V3Query('delete', 'videos', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/watermarks.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/watermarks.py deleted file mode 100644 index fe78d48c2..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/api/watermarks.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from ..query import V3Query -from ..query import query - - -# https://developers.google.com/youtube/v3/docs/watermarks/set -@query -def upload(parameters=None, data=None): - return V3Query('post', 'watermarks/set', parameters=parameters, data=data) - - -# https://developers.google.com/youtube/v3/docs/watermarks/unset -@query -def delete(parameters=None): - return V3Query('post', 'watermarks/unset', parameters=parameters) diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/query.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/query.py deleted file mode 100644 index 3ec5aacd2..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/query.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from copy import deepcopy - -from ..exceptions import ResourceUnavailable -from ..utils.logger import Log -from .request_handler import v3_request - -LOG = Log('v3', __file__) - - -class Query: - _base_url = '' - - def __init__(self, method, path, parameters=None, data=None, headers=None): - log_headers = deepcopy(headers) - log_parameters = deepcopy(parameters) - - if 'Authorization' in log_headers: # hide user's access token - log_headers['Authorization'] = '*' * 16 - - if 'key' in log_parameters: # hide api key - if len(log_parameters['key']) >= 20: - log_parameters['key'] = '****%s%s%s****' % \ - (log_parameters['key'][4:8], - '*' * (len(log_parameters['key']) - 16), - log_parameters['key'][-8:-4]) - else: - log_parameters['key'] = '*' * 16 - - LOG.debug('Initializing query: %s | %s | %s | %s | %s |' % - (method.upper(), path, log_parameters, data, log_headers)) - - if parameters is None: - parameters = {} - - if data is None: - data = {} - - if headers is None: - headers = {} - - self._method = method - self._url = self._base_url + path.lstrip('/') - self._parameters = parameters - self._data = data - self._headers = headers - - @property - def url(self): - return self._url - - @property - def headers(self): - return self._headers - - @property - def data(self): - return self._data - - @property - def parameters(self): - return self._parameters - - @property - def method(self): - return self._method - - def __str__(self): - return '{method}: {url}, {params}, {data}, {headers}' \ - .format(method=self.method.upper(), url=self.url, params=self.parameters, - data=self.data, headers=self.headers) - - def invoke(self, func): - try: - return func(self.method, self.url, self.parameters, self.data, self.headers) - except Exception as error: - raise ResourceUnavailable({ - 'error': 'resource_unavailable', - 'error_description': str(self), - 'code': '500' - }).with_traceback(error.__traceback__) - - -class V3Query(Query): - _base_url = 'https://www.googleapis.com/youtube/v3/' - - def __init__(self, method, path, parameters=None, data=None, headers=None, unauthorized=False): - # pylint: disable=import-outside-toplevel - - if parameters is None: - parameters = {} - else: - parameters = deepcopy(parameters) - - if headers is None: - headers = {} - else: - headers = deepcopy(headers) - - from .. import ACCESS_TOKEN - from .. import API_KEY - from .. import HTTP_REFERRER - - if API_KEY: - parameters.update({ - 'key': API_KEY - }) - - headers.update({ - 'Host': 'www.googleapis.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 ' - '(KHTML, like Gecko) Chrome/39.0.2171.36 Safari/537.36', - 'Accept-Encoding': 'gzip, deflate' - }) - - if HTTP_REFERRER: - headers['Referer'] = HTTP_REFERRER - - if method.lower() == 'post': - headers.update({ - 'Content-Type': 'application/json' - }) - - if ACCESS_TOKEN and not unauthorized: - headers.update({ - 'Authorization': 'Bearer {access_token}'.format(access_token=ACCESS_TOKEN) - }) - - super().__init__(method, path, parameters, data, headers) - - def invoke(self, *args, **kwargs): # pylint: disable=unused-argument - return super().invoke(v3_request) - - -def query(obj): - def wrapper(*args, **kwargs): - LOG.debug('Initiating query: %s | %s |' % (args, kwargs)) - qry = obj(*args, **kwargs) - return qry.invoke() - - return wrapper diff --git a/script.module.tubed.api/resources/lib/src/tubed_api/v3/request_handler.py b/script.module.tubed.api/resources/lib/src/tubed_api/v3/request_handler.py deleted file mode 100644 index ff976eca5..000000000 --- a/script.module.tubed.api/resources/lib/src/tubed_api/v3/request_handler.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Copyright (C) 2020 Tubed API (script.module.tubed.api) - - This file is part of script.module.tubed.api - - SPDX-License-Identifier: GPL-2.0-only - See LICENSES/GPL-2.0-only.txt for more information. -""" - -from requests import Session -from requests.adapters import HTTPAdapter - - -def _status_response(response): - if 200 <= response.status_code < 300: - payload = { - 'error': { - 'message': 'v3 Data API request was successful', - 'errors': [{ - 'reason': 'v3RequestSucceeded' - }], - 'code': response.status_code - } - } - - else: - payload = { - 'error': { - 'message': 'v3 Data API request failed', - 'errors': [{ - 'reason': 'v3RequestFailed' - }], - 'code': response.status_code - } - } - - return payload - - -def v3_request(method, url, parameters, data, headers): - adapter = HTTPAdapter(max_retries=3) - session = Session() - - session.mount('http://', adapter) - session.mount('https://', adapter) - - if data and isinstance(data, dict): - response = getattr(session, method)(url, params=parameters, json=data, - headers=headers, timeout=(2, 60)) - else: - response = getattr(session, method)(url, params=parameters, data=data, - headers=headers, timeout=(2, 60)) - - response.encoding = 'utf-8' - - try: - return response.json() - except ValueError: - return _status_response(response) diff --git a/script.module.tubed.api/resources/media/fanart.jpg b/script.module.tubed.api/resources/media/fanart.jpg deleted file mode 100644 index 6fa3d5bdd..000000000 Binary files a/script.module.tubed.api/resources/media/fanart.jpg and /dev/null differ diff --git a/script.module.tubed.api/resources/media/icon.png b/script.module.tubed.api/resources/media/icon.png deleted file mode 100644 index efdb3f7ad..000000000 Binary files a/script.module.tubed.api/resources/media/icon.png and /dev/null differ diff --git a/script.module.tubed.api/resources/settings.xml b/script.module.tubed.api/resources/settings.xml deleted file mode 100644 index 27a45b6a8..000000000 --- a/script.module.tubed.api/resources/settings.xml +++ /dev/null @@ -1,25 +0,0 @@ - - -
- - - - 0 - 52520 - - 30002 - - - - - - - - 0 - false - - - - -
-
diff --git a/script.plexmod/.gitignore b/script.plexmod/.gitignore new file mode 100644 index 000000000..59f60a7f6 --- /dev/null +++ b/script.plexmod/.gitignore @@ -0,0 +1,3 @@ +/fixRepo.py +/resources/skins/Main/TexturePacker.exe +/resources/skins/Main/packtex.bat diff --git a/script.plexmod/LICENSE.txt b/script.plexmod/LICENSE.txt new file mode 100644 index 000000000..6bcffb461 --- /dev/null +++ b/script.plexmod/LICENSE.txt @@ -0,0 +1,492 @@ + + 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 +------------------------------------------------------------------------- + + +Some of this code has been modified from the PlexApi written by Michael Shepanski + +https://github.com/mjs7231/python-plexapi + +The license for that code follows: + +Copyright (c) 2010, Michael Shepanski +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name conky-pkmeter nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------------- + +https://github.com/ValentinBELYN/icmplib + +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + + +------------------------------------------------------------------------- +Fontawesome +https://fontawesome.com/license/free +CC BY 4.0 License: https://creativecommons.org/licenses/by/4.0/# diff --git a/script.plexmod/README.md b/script.plexmod/README.md new file mode 100644 index 000000000..317bd7cab --- /dev/null +++ b/script.plexmod/README.md @@ -0,0 +1,33 @@ +# PlexMod (for Kodi) + +This is a modification of the official open-source Plex client for Kodi "plex-for-kodi" (Plex4Kodi) semi-maintained by me (pannal). + +Contrary to how this repository was handled before, this client does _not_ claim to adhere to the Plex Inc. design guidelines, all the time. + +It implements features that are not implemented in other official Plex clients and may implement others in non-conform ways. + +It is still based off of the original P4K source and critical bugfixes will be PR'd back. + +## Active branches +* [develop-kodi21](https://github.com/pannal/plex-for-kodi/tree/develop_kodi21) (Kodi 19, 20, 21 cross-compatible) +* [develop-kodi18](https://github.com/pannal/plex-for-kodi/tree/develop_kodi18) (legacy) + +Master branch is based off of the official plex-for-kodi master branch. + +## Installation + +### Via repository +* Add `https://pannal.github.io/dontpanickodi/` to your Kodi installation as a file source +* Go back to addons, choose zip file, choose the file source you added and install the repository +* Install Plex via Addons->Install from repository->Don’t Panic->Video add-ons->Plex +* Optional, recommended: Install Plextuary via Addons->Install from repository->Don’t Panic->Look and Feel->Skin->Plextuary + +### Manual +* Checkout any branch of this GitHub repository, rename to `script.plexmod` and use as an addon + + +## Help/Bug Reports +https://forums.plex.tv/t/plexmod-for-kodi-18-19-20-21/481208 + +## License +[LICENSE](https://github.com/plexinc/plex-for-kodi/blob/master/LICENSE.txt) diff --git a/script.plexmod/TESTING_SQUAD.md b/script.plexmod/TESTING_SQUAD.md new file mode 100644 index 000000000..12506a8e3 --- /dev/null +++ b/script.plexmod/TESTING_SQUAD.md @@ -0,0 +1,2 @@ +## The Squad (TM) +@sonofdibnah @ecsjjgg @Unknown3899 @THGhost @bowlingbeeg @Buttzy10169 diff --git a/script.plexmod/addon.xml b/script.plexmod/addon.xml new file mode 100644 index 000000000..f004d249e --- /dev/null +++ b/script.plexmod/addon.xml @@ -0,0 +1,49 @@ + + + + + + + + + + video + + + executable + + + + PlexMod for Kodi + Unofficial Plex for Kodi add-on + GPL-2.0-only + https://forums.plex.tv/t/plexmod-for-kodi-18-19-20-21/481208 + https://www.plex.tv + https://github.com/pannal/plex-for-kodi + all + +- Fix: certain home hubs were limited to 10 items without pagination +- Fix: very rare infinite playback loop with enabled embedded subtitles (thanks @florinvlaicu for reporting) +- Fix: Partially fix issues when switching audio streams during Transcoded/DirectStream playback +- Fix: Fix error in ExtendHubTask +- Fix: Partially revert "Home: Increase section change timeout from 0.3s to 0.5s; re-jig section change handling to allow using ENTER/SELECT to immediately select a section below 0.5s wait time" due to instability +- Fix: Fix a couple of SyntaxWarnings for invalid escape sequences +- Fix: Unfocusable buttons throw focus errors occasionally on shutdown +- Fix: Laggy UI after playback/5 minutes due to requesting certain hubs without limits applied. Drastically improving UI smoothness even for smaller hubs (below 1000 items) + This also fixes crash and shutdown issues and by proxy should enable everyone to use a poster resolution scale of at least 200%. +- Fix: Depending on the situation, the home hubs update request could be sent multiple times while waiting for the first one to finish. (thanks @bowlingbeeg) +- Subtitles: Supply OpenSubtitles.com hash if possible and enabled +- Subtitles: Add setting to Audio/Subtitles to calculate the OpenSubtitles.com hash (default: off) +- Core: Advanced Settings: make default resolution scale a percentage; limit to 750% max (based on Plex max poster size) +- Core: Add more debug logging for player events +- Core: Add more debug logging details for hub refresh events + + + icon.png + fanart.jpg + + + diff --git a/script.plexmod/changelog.txt b/script.plexmod/changelog.txt new file mode 100644 index 000000000..825da958f --- /dev/null +++ b/script.plexmod/changelog.txt @@ -0,0 +1,655 @@ +[- 0.7.3-rev2 -] +- Fix: certain home hubs were limited to 10 items without pagination +- Fix: very rare infinite playback loop with enabled embedded subtitles (thanks @florinvlaicu for reporting) +- Fix: Partially fix issues when switching audio streams during Transcoded/DirectStream playback +- Fix: Fix error in ExtendHubTask +- Fix: Partially revert "Home: Increase section change timeout from 0.3s to 0.5s; re-jig section change handling to allow using ENTER/SELECT to immediately select a section below 0.5s wait time" due to instability +- Fix: Fix a couple of SyntaxWarnings for invalid escape sequences +- Fix: Unfocusable buttons throw focus errors occasionally on shutdown +- Fix: Laggy UI after playback/5 minutes due to requesting certain hubs without limits applied. Drastically improving UI smoothness even for smaller hubs (<1000 items) + This also fixes crash and shutdown issues and by proxy should enable everyone to use a poster resolution scale of at least 200%. +- Fix: Depending on the situation, the home hubs update request could be sent multiple times while waiting for the first one to finish. (thanks @bowlingbeeg) +- Subtitles: Supply OpenSubtitles.com hash if possible and enabled +- Subtitles: Add setting to Audio/Subtitles to calculate the OpenSubtitles.com hash (default: off) +- Core: Advanced Settings: make default resolution scale a percentage; limit to 750% max (based on Plex max poster size) +- Core: Add more debug logging for player events +- Core: Add more debug logging details for hub refresh events + + +[- 0.7.2 -] +- Fix: playing next episode when episode played threshold was met when hitting STOP button +- Fix: Dropdown would roundrobin falsely onup item when initial item wasn't item 0 +- Fix: Movies: pressing back in a scrolled movie view with the header options open wouldn't back out properly +- Fix: Reset library filters when Role/Cast view opened +- Fix: Cast display in movies/preplay +- Fix: Apply dialog flicker fix in Role/Cast +- Fix: Dropdowns were broken sometimes (especially library view) +- Fix: Addon crash on non-default Kodi (e.g. OSMC) due to System.BuildVersion not matching Kodi's default +- Fix: Error on no data returned on PlexObject.reload (disconnect); Handle connection errors more gracefully +- Fix: Background of info page (thanks @bowlingbeeg) +- Fix: Rare SeekDialog crashes between episodes +- Fix: OpenSubtitles not finding subtitles for certain Movies + +- Core: Harden against network disconnects in multiple views, home and videoplayer/seekdialog +- Core: Re-enable backing out of videoplayer (inbetween screen) +- Core: Player: Alternative implementation of bingeMode auto-next; ignore stop events when triggering non-user-stop +- Core: Add setting for poster/thumbnail resolution scaling, implement everywhere sensible +- Core: Thumbnails/Art: use the same parameters as PlexWeb for images +- SeekDialog: Possible fix for OSD not coming up again in some scenarios + + +[- 0.7.1 -] +- Fix: Embedded subtitle could be set to the wrong one (edge case) +- Fix: Episodes: theme music error when clicking on seasons +- Possibly fix rare traceback when closing an episodes window while waiting for an episode reload task +- Fix: Core: only stop playback on screensaver if player is playing video (not audio, ...) +- Fix: rare UI crash for items before 1970-01-02 (yes, we've had media back then) + +- Set Accept-Language to Kodi's language setting when talking to the Plex Server, localizes subtitle titles, Library translations etc. +- Use extended title for subtitles (and add advanced/addon setting) +- Add: Show buffer state in stream info +- Reduce dialog flickering in certain situations (not fully, probably impossible) +- Player: bingeMode/autoskip credits/manual-next: avoid double-next in certain circumstances +- VideoPlayer: don't react to inputs while waiting for playback (AVStarted) to start (fixes early-back-out-crashes) +- SeekDialog: stop marker countdown on seek +- SeekDialog: auto-skip marker: ignore input while auto skipping to next video +- SeekDialog: only count down marker when OSD not shown +- SeekDialog: ignore input while handling next-video-pressed +- SeekDialog: hide OSD fast on ACTION_PAUSE/ACTION_PLAYER_PLAY/ACTION_PLAYER_PLAYPAUSE and OSD is shown +- Player/SeekDialog: Fix crash when handling SIGINT (alt+f4 on windows) +- Hubs: make all TV/movie hubs as hub types that receive updates after items being watched or marked (un) watched +- Settings: Disable alternative home hubs refresh by default +- ServerList: further hardening, list shouldn't visually "crash" anymore in certain conditions +- Pagination/Episodes: allow right round-robin on paginated long item lists (>26) +- Movies: Reorder Cast/Related/Extras according to PlexWeb +- Movies: Add reviews +- Cast: Increase Cast picture quality everywhere +- Core/Settings: Adjust recommended cache/buffer/readfactor values for different Kodi versions, starting with 21.0-BETA2 (20.90.821) +- Core/Settings: Incorporate new Kodi cache/readfactor values; clamp cache values to divisible by 16 +- Serverlist: allow roundrobin at the bottom boundary +- StreamInfo: hide OSD after closing streaminfo +- Settings: Add setting to use Kodi keyboard for searching (default: off) +- Main: harden main loop (still not exiting properly when exiting fast after server change; reinits previously inited windows on exit, sometimes) +- SubtitleDownload/OpenSubtitles.com: set global videoinfo infoLabels to improve subtitle search hinting (waiting for PR to be accepted) + + +[- 0.7.0 -] +- Fix kiosk mode startup issue when other modal dialogs were active when PM4K was trying to start in kiosk mode +- Fix video OSD hiding too fast in certain circumstances +- Fix photoplayer issues; skip ugly initial loading image; show loading state only when it takes longer than 500ms; fix prev/next not always working; increase speed by x10 +- Fix episodes view replaying the theme music when returning from a different-season-view +- Fix mediaBufferSize reported to the Plex MDE endpoint was always empty (resulting in Plex assuming we only have 5 MB cache, leading to all sorts of buffer issues) +- Fix pin entry with J characters looking like an uppercase i +- Fix episode regularly ending in bingeMode (not credits skipped) still showing post play +- Fix enabled subtitles always leading to a transcode (and subtitle burn-in) of the video stream in DirectStream scenarios +- Fix player UI for transcoded/DirectStream sessions + +- UI: background/transition/rendering core rework, reducing flickering, "bouncy" background fades, improving overall snappiness of the interface (dialog transition flicker not solved) +- UI: remove the [...]-loading-spinner for everything that takes under 500ms, making the UI more natural +- UI: crossfade backgrounds before entering a view, if possible, reducing the animation load between view transitions +- UI: support more elements as background-providers (e.g. photos) +- Add advanced/plugin setting to enable/disable background crossfading altogether +- Add setting to stop video playback when screensaver (Kodi) is activated +- Add setting to stop video playback after a set idle time +- Increase home section change timeout from 0.3s to 0.5s and allow immediate section selection using ENTER + +- Completely rework the AC3 handling, allowing to select in which channel configurations one wants to transcode to AC3 +- Allow subtitles to be DirectStreamed; resulting in possibly ONLY the audio being transcoded +- Add setting to burn in SSA/ASS subtitles when DirectStreaming. When disabled, they're converted to raw text and DirectStreamed, if enabled (previous default), they lead to the video stream being transcoded +- Enable markers (intro/credits) functionality in transcoded/DirectStream sessions +- Harden chapter selection logic in player UI +- SeekDialog: Unify time formatting as much as possible between transcoded and directplay modes +- Add setting to allow or deny auto-skip functionality when transcoding/DirectStreaming +- Movies preplay: raise the ratings up by 70 pixels to be in line with the title, fixes issues with certain skins +- Core: improve skin compatibility: font30 is no more as we're not using Confluence but Estuary as default. Replace with its fallback, font13 +- Core: improve skin compatibility: font16 is no more as we're not using Confluence but Estuary as default. Replace with its fallback, font13 +- Core: allow DirectStreaming of webvtt subtitles in Kodi >= 20 +- AdvancedSettings: reset background blur and opacity to their new defaults +- BGM: store last "good" audio volume before playing theme music, and apply it upon start of the addon, in case the volume reset after BGM failed/was skipped +- Core: Harden and speed up server change + + +[- 0.6.5+rev5 -] +- Add support for urllib3 >= 2.1.0 + + +[- 0.6.5+rev4 -] +- Fix dropdown roundrobin needing two UPs if first selected item is item 0, introduced in 0.6.5 +- Theme music: Harden BGM handler; wait for correct volume to be re-set, might fix not-so-rare stale volume issue +- Episodes: Show TV Show's other seasons in episodes window + + +[- 0.6.5 -] +- Fix rare postplay still shown on bingemode/autoskip credits +- Fix quick subtitle selection when override forced advanced setting is active +- Fix rare round robin case skipping item 0 onup in dropdowns +- Fix episode progress and time remaining not updating (thanks @bowlingbeeg) +- Fix embedded subtitle display delay when Cycling/using buttons to switch subtitles +- Fix rare subtitles not shown issue + +- Add: Allow video player UI buttons customization via settings; remove a couple of player navbar buttons by default +- Add "Previous Subtitle" to quick subtitles nav item +- Remember subtitle actions (prev, next, download, delay) via quick nav +- Use our own CycleSubtitles/ShowSubtitles implementation; +- Rely even less on Kodi's Player states for subtitles +- Parse and show SDH/HI flag for subtitle streams +- Hide OSD faster after closing settings; try hiding OSD faster in general if necessary +- Lose orange fallback BG on userselect as well by default +- Settings: rename Player to Player UI, rename Player (user-specific) to Playback (user-specific) +- Settings: Move forced subtitles fix from plugin settings to settings interface audio/subtitles +- Store last background URL in settings if dynamic backgrounds is active; use last BG url on startup to prevent blank BG +- Reduce embedded subtitle display backseek to 100ms +- Use episode thumbnails in continue hub if available, by default; add advanced setting +- Set default background blur to 0 (was 4), background opacity to 20 (was 30) +- Disable legacy background image fallback by default; add advanced setting +- Core improvements + + +[- 0.6.4 -] +- Fix preplay mediachoice issues +- Fix seeking with auto-skip scenarios on slower devices +- Fix online/trailer quality issues (was always using a low quality before) +- Fix round-robining in episode view +- Fix Kodi 18 +- Fix some rare Kodi playback errors in logs, mostly cosmetic +- Fix player sometimes not sending the last timeline state after playback ends +- Fix playback issues when multiple versions exist but one of them is missing +- Fix embedded subtitle display delays on playback start and on subtitle change while playing back (backseeks 1 second) +- Fix double-back-button necessary to exit when cancelling autoskip +- Fix chapters not showing when no markers present +- Fix User Pin entry not showing errors +- Fix reactivating addon after minimize +- Fix autoskip marker continuing to count down when paused; don't hide autoskip marker when paused + +- Add support for multiple media versions and fix individual stream selection support +- Add support for stream selection in trailers/extras +- Add current cache/buffer state to progress bar +- Add video/audio/subtitle stream selection for trailers +- Add zero drift playback timers for current time, time left, ends at +- Add minimize option to exit +- Add playback settings manager to enable full settings per TV show instead of just binge mode toggle; +- Add slow connection support (e.g. in a hotel); waits for the buffer to fill to a reasonable amount until playback; sets readfactor to 20 (don't use with passthrough) +Migrate previous binge mode data to new extensible format, stored as addon_data/playback_settings.json + +- Player: Better/more immediate intro-auto-skip implementation +- Cache/Buffer: Allow 26% and 30% "overcommit" (+android 23%) +- Cache/Buffer: Add readfactor to settings (needs Kodi restart) +- Cache/Buffer: Add support for new kodi-omega-master Cache/Buffer GUI settings +- Harden next/prev episode handling in auto-skip scenarios +- Harden next/prev button handling +- Settings: allow vertical round-robining for settings and options lists +- User Switch: Allow cancellation of switch; select the current user by default when switching + + +[- 0.6.2 -] +- Add system setting to execute action upon sleep event (e.g. exit Kodi when display goes to sleep; fixes passthrough issues on SHIELD) +- Allow canceling postplay timeout with enter/OK +- Binge mode: show postplay if video was exited manually +- Show local/LAN server status in status bar and server list via new icon; server list is live now +- Discover local/LAN servers automatically (if any of the plex.tv/resource's DNS points to a local IP and is reachable) +- plexapp/account/main: init account earlier to pick up ID early; slightly reorder initialization sequence to pick up preferred servers earlier +- Connection logic: refactor connection significance; wait for manual connections if necessary/wanted; add way more logging (as this might break things) +- Remember last used server per user +- Refactor seekdialog autoskip handling and chapter/marker display logic +- Harden the episodeswindow's paginator, making it less easy to generate a huge amount of requests when paging quickly (holding down left/right) +- Harden chapter/marker autoskip +- Add "combined chapters", intelligently merging chapters and markers into one, if available +- Add BIF preview images to markers if available ("Enable video preview thumbnails" needs to be enabled on server and library) +- Add separate "show autoskip info early" offset to addon settings (default: 2.5s) +- Add countdown to autoskip info button, informing the user that we're about to auto skip the next marker (intro/credits) +- Set skip intro button show early threshold to 60s (was 120s) to skip recaps but only those +- Seek as fast as possible on immediately occurring intro; skip directly to next video if possible on last credits with autoskip enabled +- Add separate connectivity check timeout; properly set up timeouts for async requests; log ping in local network tests +- Harden server discovery; further improvements to preferring local over secure +- Update reachability of servers live in server list; +- Remove server refresh button due to issues; +- Add fontawesome license +- Show busy dialog when selecting a new user, preventing errors +- Trigger force available servers/connections refresh when changing network settings; + +- Fix Bingemode not properly stored per user +- Fix multiple credits naming in seekdialog +- Fix error upon episodes window reinit; catch error upon missing listitem +- Remove time left tag after watching more than one episode +- Add some missing advanced/addon setting descriptions; cleanup +- Disable GDM discovery by default +- Fix late display of embedded subtitles; the change has been forgotten, sorry; fixes #54 + + +[- 0.6.0-rc1 -] +- Add Kodi Buffer/Cache settings to new System tab +- Add pm4k_cache_template.xml to allow further customization (copy to profile folder) +- Add addon path and Kodi profile path to Settings/About +- Add advanced/addon setting to set (Plex) HTTP request timeouts; change default from 10 to 5 (on display/crash issues, increase timeout) +- Add explicit LAN check for servers (docker-based ones won't show as local by default); only works on Kodi 19 and above; add icmplib and license +- Add setting to specify LAN check connection timeout (default 10ms) +- If desired, prefer an insecure local server over a secure one after switching users, as well +- Add warning when preferring local server connections over secure ones +- Add long timeout for essential plex.tv requests +- Add force server list refresh button to server list (allows reloading the available servers after changing PMS network settings) +- Add setting to verify local/LAN connections even if plex.tv doesn't think they're local +- Add setting to prefer local connections over secure ones, enabling enforced manual servers +- Add advanced/addon setting to add an offset to intro/credits autoskip, as the Plex markers might be a little early (default +2 seconds) + +- Show video chapters if available in playback instead of bigSeek (also add a setting) +- Show virtual chapters in playback (from Plex markers, intro, credits etc.) if no chapters exist for a video file +- Reload items properly with chapters upon preplay-reinit +- Reorder settings, advanced is now system, add network settings +- Show chapters and/or markers in info view of a video item + +- Implement TV binge mode (auto skip intros if not first EP of season; auto skip credits; skip postplay) +- Implement per-user settings +- Make Binge-mode overridable per show +- Make Binge-mode, all auto-skip and whether to display chapters or markers a per-user setting + +- Fix focusing the wrong episode after opening the info screen +- Fix reloading episodes list after watching multiple episodes +- Strip out MP4 file name in stream URL to prevent subtitles trying to be downloaded, fixing long wait time (thanks @microadam) +- Fix failure on video with no audio stream +- Optimize remaining time calculations for preplay screen + + +[- 0.5.5-rev2 -] +- Fix auto intro skip not working after skipping to next episode +- Skip postplay when skipping to next video from player +- Limit early-show-intro-skip to intros occurring in the first 2 minutes; +- add advanced setting to customize this value +- Add option to show and episodes-windows (they toggle their show) to override the global auto-skip-intro setting for a show + + +[- 0.5.5 -] +- Add main setting to show the intro skip button early; enables recap skipping; works with auto-intro-skip! +- Move some boolean settings from Main>Advanced to Main +- Add main setting to use alternative home hubs refresh method introduced by @bowlingbeeg; default on +- Add main setting to specify item-played-threshold in main settings, please sync with your Plex Server; possibly fixes edge cases (postplay/next-overskip issues) +- Fix postplay timer-circle display on later (Nexus++?) Kodi versions +- Rework time formatting and hour padding detection for "Ends at" display completely +- Change watched status to also look at resume offset (thanks @bowlingbeeg) +- Fix crash when background threads are still running on library exit (thanks @bowlingbeeg) +- Fix playlist issues (thanks @bowlingbeeg) +- Change look of letter jump list (thanks @bowlingbeeg) +- Fix sorting/filtering issues (thanks @bowlingbeeg) +- Add second view type for episodes and albums (thanks @bowlingbeeg) +- Support showing collections inline (thanks @bowlingbeeg) +- Add support for smaller poster size view (thanks @bowlingbeeg) + + +[- 0.5.4 -] +- Store user thumbnail in cache (improves home "load" time) +- Change client identifier from Plex-for-Kodi to PM4K; + possibly fix "PlexNet" entries in Authorized Devices; might also break stuff, please report back +- fix closing quote in chinese translation +- Add missing German translations +- Add "Ends at" to player ends-at time by default; add option to disable this label +- Add fallback for missing audiooutput.channels setting (seems to affect libreelec only); relates to #48 +- Change resume dialog to show resume time (thanks @bowlingbeeg) +- Add unwatched status to pre-play screen and add progress bars to more hubs (thanks @bowlingbeeg) +- Fix missing parameters on Plex API queries (thanks @bowlingbeeg) +- Add time left to pre-play screen (thanks @bowlingbeeg) +- Fix audio/subtitle selection not updating correctly in pre-play screen (thanks @bowlingbeeg) + + +[- 0.5.3 -] +- Inhibit screensaver when in photo slideshow (Kodi 19 and above) +- Add addon setting to use old profile from P4K instead of the optimized one (might fix 3D issues with DP; not sure why?); default: off +- Show video stream render type in preplay/epidoses screen (SDR/HDR/DV/HLG) +- Adjust default background image blur/opacity from 0/40 to 4/30 +- Add addon setting to set solid background colour instead of pictures (Kodi 19 and above) +- Add addon setting to hide when current media will end (default: shown, as before) +- Split filename in info screen to avoid overflow +- Don't show "no content" on home; amended PR #43 +- Bad temporary fix for photo playqueues containing videos +- Photos: Use Kodi slideshow interval; fixes #41 +- Add Simplified Chinese Translation (thanks @Liqianyu) +- Fix AV1 setting description +- Fix single-season pagination (anime) +- Update german translation (thanks @jamal2362) +- Fix issue when displaying a library that has no hubs to display (thanks @bowlingbeeg) +- Fixed an issue with video playlists and the general play button not working (thanks @bowlingbeeg) +- Fixed a couple of places that require integer division (thanks @bowlingbeeg) + + +[- 0.5.2-beta0.1 -] +re-add support for leia (Kodi 18) + + +[- 0.5.1 -] +add media/part/stream info to info screen +read and honor advancedsettings/memorysize + + +[- 0.5.0 -] +implement multiple markers per video file +add skip credits Plex-style (skip to post-credit scene if applicable or skip to end) +add auto skip credits feature +actually respect DirectPlay=Off and DirectStream=Off +fix directstream/transcoding (DS never worked apart from h264) +fix transcoded audio always being MP3 +deviate from Web-client-based profile to own dynamic profile +allow directplay/stream/transcode of virtually anything on Kodi +add settings to force directstream/transcoded audio to AC3 and/or DTS (for passthrough) +add setting to limit directstream/transcoded audio to original audio bitrate/channels +add setting to limit directstream/transcoded audio to Kodi audio channel setting +prevent PMS transcoding audio to higher channel count than original audio +show video codec in preplay screen +settings: round-robin when at the end of the left heading list +stream status dialog: show HW encoding state +replace progressive jpg files (thanks @shyzus) + +note: massive thanks to @hbbs for reporting the MP3 issue and sticking with me for the whole process of figuring out how to do audio stuff in a Plex client + + +[- 0.4.6 -] +add more sort options for ratings where applicable (rating, audienceRating, contentRating, userRating) +add smart desc/asc default sort (need feedback) +use PlexWeb sort field naming (except for PlexWeb Rating=My Rating which is clearer) +note: TV shows "By Audience Rating" seems to be broken in Plex +previous beta fixes +small bugfixes + + +[- 0.4.5-beta1.3 -] +properly refocus after stopping music via the mini player fix msgstr +reset autoseektimer prematurely before auto skipping intro +stream details: don't show unknown values +some music fixes; internals +auto intro skip: skip only once, show manual skipping otherwise, measures for preventing wrong jump +fix HEVC default + + +[- 0.4.5-beta1.2 -] +fix music and photo handlers (thanks @Buttzy10169) +fix next/prev handlers in general +fix previous release's issues +use different photos temp path resolution +enable HEVC by default + + +[- 0.4.4 -] +community fixes (thanks @Buttzy10169 @fvlaicu) +add auto-skip intro setting (default: off) +add addon-setting to customize how long skip-intro button is shown (default: 10s) + + +[- 0.4.3 -] +Allow skip-intro for users on a plexpass enabled server (thanks @fvlaicu) +Fix visual issues (due to botched addon rename) +Make AV1 setting Kodi major version dependant; pave way for hybrid codebase for 19/20 +Implement new network discovery mech on windows (GDM) using powershell (experimental) +Possibly fix GDM on windows (especially on non-english variants) +Possibly fix GDM on Linux (non-BSD) + + +[- 0.4.2 -] +Fix skip intro button +Fix duplicate addon setting +Enable dynamic backgrounds by default set defaults for dynamic backgrounds: blur=0, opacity=40 +Fix kiosk mode (autorun plex on kodi startup) +Fix music playback +Fix theme music playback; set default theme music volume to 50% + + +[- 0.4.1 -] +Initial PlexMod for Kodi release, based on my plex-for-kodi fork's develop branch +Add support for Kodi Nexus (20) on LibreELEC and Android +Add AV1 toggle and AV1 direct play support +Rename addon to "script.plexmod" +Add all non-plex-conform features of the develop branch as default features + +[- 0.2.3 -] + +Fix error causing playback to fail + +[- 0.2.2 -] + +Added support for collections in library views +Update Polish strings (thanks to Zachar) +Fix GDM issue (thanks to MrPumo) +Fix Python 3 issues (thanks to MrPumo and pannal) + +[- 0.2.1 -] + +Added Skip Intro (thanks to pannal) +Use device name for friendly name +Fix some hubs not displaying +Added title scrolling when focused on some items +Show playback speed on OSD +Fix possible error on playlists window + +[- 0.2.0 -] + +Bump version to 0.2.0 to differentiate from deprecated Kodi 17 0.1.x versions + +[- 0.1.8 -] + +Fix seek resetting when clicking select on seek bar with OSD visible +Add Spanish translations (thanks to rdcalle) +Add Portuguese (Portugal) translations (thanks to Generator) +Make python 3 compatible (thanks to pannal) + +[- 0.1.7 -] + +Fix transcode failures on Windows (possibly other platforms) +Add Italian translations (thanks to iz8mbw) +Use Kodi time format setting (Issue #90) (thanks to pannal) +Restore last window when starting the add-on while it is running (Issue #126) +Add Portuguese (Brazil) translations (thanks to DiogoAbu) +Add ability to exit search edit box with previous menu (escape) button (Issue #194) (thanks to pannal) +Only play unwatched media when pressing play or shuffle and filtering for unwatched (thanks to tlt21) +Fix issue with duplicated icons when fast-scrolling a hub (Issue #177) (thanks to pannal) +Fixed GUI not responding after public IP renewal (thanks to pannal and mkliche) +Advanced setting: Automatically seek selected position after a delay (Issue #172) (thanks to pannal) +Advanced setting: Use skip step settings from Kodi (Issue #133) (thanks to pannal) +Advanced setting: Use Plex/Kodi steps for timeline (Issue #133) (thanks to pannal) +OSD-Autohide improvements (thanks to pannal) +Show post play on user stop when item is considered played (thanks to pannal) +Use Unplayed/Played instead of Unwatched/Watched (thanks to pannal) +Fix issue with cast popup displaying partially offscreen (thanks to pannal) +Various fixes for incorrectly transcoded playback (Issue #201) (thanks to pannal) +Support new subtitles menu in playback for Leia (thanks to pannal) +Make sure progress bar is visible on items with less than 2% watched (thanks to pannal) +Fix focus issues caused when hubs are removed, i.e. On Deck, Continue Watching (thanks to pannal) +Fix issues with flattened seasons +Allow Kodi info display to be visible when hitting the info button +Fixes hubs not updating and some crash issues +More fixes for Kodi 18, ex. missing letters list (Issue #265) +Fix resume behavior with DSPlayer (thanks to pannal) +Add Polish translations (thanks to Zachar2) +Playlist and album view tweaks (thanks to pannal) +Quality selection clean up and fixes (thanks to pannal) +Fix to allow 3D mode selection +Add advanced option to skip the header when going back (thanks to pannal) +Ensure paused state is retained after seeking or changing video settings (thanks to pannal) +Allow closing of the options header in scrolled views using context menu button (thanks to pannal) +Some direct playback fixes (thanks to pannal) +Some shutdown fixes (thanks to pannal) +Show play state indicators to related items in preplays and On Deck in postplay (thanks to pannal) +Fix some resume issues (thanks to pannal) + +[- 0.1.6 -] + +Fix issues when selecting subtitles for users of certain languages (Issue #188) + +[- 0.1.5 -] + +Allow direct playback of episodes and movies using the play button on hub screen (thanks to pannal) +In progress watched items had the 'Mark unwatched' option instead of 'Mark watched' +Fix an issue causing the home window to start slightly scrolled down if it was closed scolled down +Fix an issue causing an error when sort was sort was "BY UNWATCHED" and the filtering "UNWATCHED" (Issue #187) +Fix issue with letter shortcuts sometimes being shown/not shown incorrectly +Fix a bug causing crash and reboot on startup on Raspberry Pi (LibreElec specific?) - Reverts "Fix autostart to work with profiles" + +[- 0.1.4 -] + +Kodi v18: Fix skin compatability issues +Kodi v18: Fix incompatibility with requests module update +Kodi v18: Fix issue with playback not resuming properly when direct playing +Kodi v18: Fix issue with progress bar not moving on music player/music playlist screens +Fix album display on artist screen +Fix bug causing failure to show audio player when clicking the mini player from some screens +Show original artist when available (Issue #143) +Improve centering over label backgrounds on pre-play screens +Fix some spots where tokens could still be logged +Play correct track when playing a multi-disc album (Issue #148) +Improve headers for better identification in 'Now Playing' and future server profile +Updated German language strings (thanks to coffinbearer) +Fix French translations file (thanks to coffinbearer) +Fix some Hungarian translations (thanks to vampywiz17) +Add Russian translations (thanks to shady2k) +Fix autostart to work with profiles (thanks to pannal) +Fix focus loss on home screen due to error (Issue #169) +Fix "No Servers Found" when preferred server is unavailable (Issue #159) +Fix system keyboard not working for search (Kodi 17+)(Issue #165) +Fix issue causing hubs to scroll up when settings were opened +Fix failure to sign in when signed off externally (Issue #110 & #117) + +[- 0.1.3 -] + +Fix incompatibility with requests module update + +[- 0.1.2 -] + +Fix issue with hubs not showing with (newer?) server versions + +[- 0.1.1 -] + +Remove PlexPass disclaimer from add-on info + +[- 0.1.0 -] + +Improved hub vertical scrolling animation +Fix (#87): Switching between managed users causes Kodi to crash +Fix (#119): Occasional crashes when exiting the add-on +Remove PlexPass requirement + +[- 0.0.102 -] + +Handle connection errors on link screen polling +Handle skip next/prev when playing video playlists +Home Screen: Navigating to the 'More' button now causes an update for all movement +Add ability to view episodes/albums in library view +Add German translation (thanks to coffinbearer) +Add Czech translation (thanks to Pavuucek) +Add Hungarian translation (thanks to vampywiz17) +Add French translation (thanks to raffoul/rlimbach) +Fix issue #64 (multi part movies won't playback) +Improve timeline reporting +Friendly name now includes hostname to differentiate Kodi devices +Possible fix for issue #102 +Fix a bug causing an error on server decisions +Fix errors caused by localization on episodes and pre-play screens +Fix some errors caused when no internet + +[- 0.0.101 -] + +Fix (Issue #68): Artist screen options button was showing Mark Unwatched +Fix (Issue #65): When returning from playback episode pre-play did not update the selected episode +Fix: Codec info button ('o') handling on Krypton+ +Remove mkv/hls testing option in add-on Kodi settings +Add Debug Logging option to Advanced Settings +Fix: Post-play after the last item in the playlist was replaying the last item instead of the displayed up next item + +[- 0.0.100 -] + +Fix some stability issues +Fix encoding error on startup +Improve fallbaack background quality +Fix an error causing opening of photo directories to fail +Disable debug logging by default +Make Home hub square items smaller +Improve startup and shutdown times +Fix some server reachability testing bugs and improve performance +Possible fix for crashing when shutting down Kodi while the add-on is running +Add workaround to allow launching from My Add-ons and shortcuts +Fix text cutoff for post-play in Estuary +Prevent the add-on from starting another instance on update +Skip user select in offline mode +Show 'Offline Mode' item in user options when in offline mode - selecting attempts going online + +[- 0.0.99 -] + +Fix: Episodes were not resuming + +[- 0.0.98 -] + +Fix: Was allowing non-Plex-Pass users +Fix: Some videos failed to play with certain audio stream selections +Fix: Some videos failed to play due to a logging error + +[- 0.0.97 -] + +Add 'Play Version...' to the pre-play options (Issue #34) +Added spring loaded playlists to Playlist section on Home screen (Issue #28) +Fix playlist composite images +Fix: Extending a hub again after a section change was starting where the previous extension left off +Handle no content and 404 for section hubs on Home screen (Issue #52) +Handle no content or no content for filter on library screen +Handle videos without duration set +Some Home screen hub loading optimization + +[- 0.0.96 -] + +Remove PIL usage +Use plex.tv instead of my.plexapp.com when requesting pin (Issue #50) + +[- 0.0.95 -] + +Now remembers unwatched filter state and sorting selection on library views (Issue #44) +Fix: Local media was being treated as remote media for playback decisions +Added a dialog to search for when selected playlists are not accessible by the current user (Issue #42) +Fixed font issues on episode/movie pre-plays and settings screens (Issue #45) + +[- 0.0.94 -] + +Fix: Filtering for unwatched movies was causing nothing to be displayed (regression) +Fix (Issue #41): Incorrect strings were being displayed on the episode pre-play delete dialog +Playlists now load in progressively in the background (Issue #35) +Added a dialog on failure to connect when getting sign-in pin (Issue #32) +Change posters view so that two rows are always visible + +[- 0.0.93 -] + +Fix: (Issue #20) Filtering by unwatched still showed watched items +Fix: (Issue #33) Photo directories were failing to open from the Home screen +Fix: (Issue #33) Some photos failed to open from the Home screen +Fix: Disabling the 'Allow HEVC' setting had no effect +Added a busy indicator while photos are loading +Fixes for DVR episodes without episode numbers + +[- 0.0.92 -] + +Official Kodi repo submission +Only change audio streams when necessary + +[- 0.0.91 -] + +Fix (Issue #23): Pressing a play on a show was not starting on the first unwatched or in progress episode +Implement (Issue #30): Decision endpoint handling for proper interaaction with streaming brain - fixes related failed playback issues +Various minor bug fixes causing fanart screen stalls, etc + +[- 0.0.90 -] + +Fix: (Issue #13) Errors caused by encoding issues in episodes pre-play and track playback +Fix: Player settings dialog would sometimes not close without some user input +Fix: (Issue #25) Increased music playback thumbnail resolution - affects add-on player screen and skin views outside the addon +Fix: (Issue #24) Items in movie playlists were not playable +Change: Player settings dialog now puts focus on the selected setting when displaying setting lists +Change: (Issue #21) Now back button only jumps to top bar when top bar is off screen + +[- 0.0.89 -] + +Fix: Selecting non plex pass not managed users caused sign out on addon restart +Fix: Playlists view showed empty space when no audio playlists were present and prevented focus + +[- 0.0.88 -] + +Fix: Starting a video while audio was playing triggered an error causing issues with OSD/control +Fix: Selected audio track was not being played with direct play + +[- 0.0.87 -] + +Initial public beta + +[- 0.0.1 -] + +Initial development \ No newline at end of file diff --git a/script.plexmod/default.py b/script.plexmod/default.py new file mode 100644 index 000000000..3f0bb5be7 --- /dev/null +++ b/script.plexmod/default.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +from lib import main + + +main.main() diff --git a/script.plexmod/fanart.jpg b/script.plexmod/fanart.jpg new file mode 100644 index 000000000..7548349f6 Binary files /dev/null and b/script.plexmod/fanart.jpg differ diff --git a/script.plexmod/icon.png b/script.plexmod/icon.png new file mode 100644 index 000000000..1a6ba1469 Binary files /dev/null and b/script.plexmod/icon.png differ diff --git a/script.plexmod/lib/__init__.py b/script.plexmod/lib/__init__.py new file mode 100644 index 000000000..92224dc3f --- /dev/null +++ b/script.plexmod/lib/__init__.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import +import requests + +# Disable some warnings. These are not security issue warnings, but alerts to issues that may cause errors +try: + from requests.packages.urllib3.exceptions import InsecurePlatformWarning + requests.packages.urllib3.disable_warnings(InsecurePlatformWarning) +except: + import traceback + traceback.print_exc() + +try: + from requests.packages.urllib3.exceptions import SNIMissingWarning + requests.packages.urllib3.disable_warnings(SNIMissingWarning) +except: + # probably urllib3 >= 2.1.0 + pass + +from . import compat +from . import _included_packages diff --git a/script.plexmod/lib/_included_packages/__init__.py b/script.plexmod/lib/_included_packages/__init__.py new file mode 100644 index 000000000..52d8b592a --- /dev/null +++ b/script.plexmod/lib/_included_packages/__init__.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +import os +import sys +import inspect + +cmd_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0])) +if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) diff --git a/script.plexmod/lib/_included_packages/icmplib/__init__.py b/script.plexmod/lib/_included_packages/icmplib/__init__.py new file mode 100644 index 000000000..6770e6988 --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/__init__.py @@ -0,0 +1,44 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + +from .sockets import ICMPv4Socket, ICMPv6Socket, AsyncSocket +from .models import ICMPRequest, ICMPReply, Host, Hop +from .ping import ping, async_ping +from .multiping import multiping, async_multiping +from .traceroute import traceroute +from .exceptions import * +from .utils import is_hostname, is_ipv4_address, is_ipv6_address +from .utils import PID, resolve, async_resolve + + +__author__ = 'Valentin BELYN' +__copyright__ = 'Copyright 2017-2023 Valentin BELYN' +__license__ = 'GNU Lesser General Public License v3.0' + +__version__ = '3.0.4' +__build__ = '231010' diff --git a/script.plexmod/lib/_included_packages/icmplib/exceptions.py b/script.plexmod/lib/_included_packages/icmplib/exceptions.py new file mode 100644 index 000000000..b37646bfa --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/exceptions.py @@ -0,0 +1,214 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + + +class ICMPLibError(Exception): + ''' + Exception class for the icmplib package. + + ''' + + +class NameLookupError(ICMPLibError): + ''' + Raised when the requested name does not exist or cannot be resolved. + This concerns both Fully Qualified Domain Names and hostnames. + + ''' + def __init__(self, name): + message = f'The name \'{name}\' cannot be resolved' + super().__init__(message) + + +class ICMPSocketError(ICMPLibError): + ''' + Base class for ICMP sockets exceptions. + + ''' + + +class SocketAddressError(ICMPSocketError): + ''' + Raised when the requested address cannot be assigned to the socket. + + ''' + def __init__(self, address): + message = f'The requested address ({address}) cannot be ' \ + 'assigned to the socket' + super().__init__(message) + + +class SocketPermissionError(ICMPSocketError): + ''' + Raised when the privileges are insufficient to create the socket. + + ''' + def __init__(self, privileged): + if privileged: + message = 'Root privileges are required to create the socket' + else: + message = 'A prior configuration of your OS is required ' \ + 'to use ICMP sockets without root privileges. ' \ + 'Read more on https://github.com/ValentinBELYN' \ + '/icmplib' + + super().__init__(message) + + +class SocketUnavailableError(ICMPSocketError): + ''' + Raised when an action is performed while the socket is closed. + + ''' + def __init__(self): + message = 'The socket can no longer be used after its closure' + super().__init__(message) + + +class SocketBroadcastError(ICMPSocketError): + ''' + Raised when a broadcast address is used and the corresponding option + is not enabled on the socket. + + ''' + def __init__(self): + message = 'Broadcast is not allowed: ' \ + 'please use the \'broadcast\' property to allow it' + super().__init__(message) + + +class TimeoutExceeded(ICMPSocketError): + ''' + Raised when a timeout occurs on a socket. + + ''' + def __init__(self, timeout): + message = f'The timeout has been reached ({timeout}s)' + super().__init__(message) + + +class ICMPError(ICMPLibError): + ''' + Base class for ICMP error messages. + + ''' + def __init__(self, message, reply): + super().__init__(message) + self._reply = reply + + @property + def reply(self): + return self._reply + + +class DestinationUnreachable(ICMPError): + ''' + Base class for ICMP Destination Unreachable messages. + + Destination Unreachable message is generated by the host or its + inbound gateway to inform the client that the destination is + unreachable for some reason. + + ''' + _CODES = {} + + def __init__(self, reply): + if reply.code in self._CODES: + message = self._CODES[reply.code] + else: + message = f'Destination unreachable, bad code: {reply.code}' + + super().__init__(message, reply) + + +class ICMPv4DestinationUnreachable(DestinationUnreachable): + _CODES = { + 0: 'Destination network unreachable', + 1: 'Destination host unreachable', + 2: 'Destination protocol unreachable', + 3: 'Destination port unreachable', + 4: 'Fragmentation needed and DF set', + 5: 'Source route failed', + 6: 'Destination network unknown', + 7: 'Destination host unknown', + 8: 'Source host isolated', + 9: 'Destination network prohibed', + 10: 'Destination host prohibed', + 11: 'Destination network unreachable for ToS', + 12: 'Destination host unreachable for ToS', + 13: 'Packet filtered', + 14: 'Precedence violation', + 15: 'Precedence cutoff' + } + + +class ICMPv6DestinationUnreachable(DestinationUnreachable): + _CODES = { + 0: 'No route to destination', + 1: 'Communication with destination administratively prohibited', + 2: 'Beyond scope of source address', + 3: 'Address unreachable', + 4: 'Port unreachable', + 5: 'Source address failed ingress/egress policy', + 6: 'Reject route to destination' + } + + +class TimeExceeded(ICMPError): + ''' + Base class for ICMP Time Exceeded messages. + + Time Exceeded message is generated by a gateway to inform the source + of a discarded datagram due to the time to live field reaching zero. + A Time Exceeded message may also be sent by a host if it fails to + reassemble a fragmented datagram within its time limit. + + ''' + _CODES = {} + + def __init__(self, reply): + if reply.code in self._CODES: + message = self._CODES[reply.code] + else: + message = f'Time exceeded, bad code: {reply.code}' + + super().__init__(message, reply) + + +class ICMPv4TimeExceeded(TimeExceeded): + _CODES = { + 0: 'Time to live exceeded', + 1: 'Fragment reassembly time exceeded' + } + + +class ICMPv6TimeExceeded(TimeExceeded): + _CODES = { + 0: 'Hop limit exceeded', + 1: 'Fragment reassembly time exceeded' + } diff --git a/script.plexmod/lib/_included_packages/icmplib/models.py b/script.plexmod/lib/_included_packages/icmplib/models.py new file mode 100644 index 000000000..c055fdf13 --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/models.py @@ -0,0 +1,491 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + +from .exceptions import * +from .utils import random_byte_message + + +class ICMPRequest: + ''' + A user-created object that represents an ICMP Echo Request. + + :type destination: str + :param destination: The IP address of the host to which the message + should be sent. + + :type id: int + :param id: The identifier of the request. Used to match the reply + with the request. In practice, a unique identifier is used for + every ping process. On Linux, this identifier is automatically + replaced if the request is sent from an unprivileged socket. + + :type sequence: int + :param sequence: The sequence number. Used to match the reply with + the request. Typically, the sequence number is incremented for + each packet sent during the process. + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type ttl: int, optional + :param ttl: The time to live of the packet in terms of hops. + Default to 64. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of the ICMP packet. + Provides a defined level of service to the packet by setting the + DS Field (formerly TOS) or the Traffic Class field of the IP + header. Packets are delivered with the minimum priority by + default (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. + + ''' + __slots__ = '_destination', '_id', '_sequence', '_payload', \ + '_payload_size', '_ttl', '_traffic_class', '_time' + + def __init__(self, destination, id, sequence, payload=None, + payload_size=56, ttl=64, traffic_class=0): + + if payload: + payload_size = len(payload) + + self._destination = destination + self._id = id & 0xffff + self._sequence = sequence & 0xffff + self._payload = payload + self._payload_size = payload_size + self._ttl = ttl + self._traffic_class = traffic_class + self._time = 0 + + def __repr__(self): + return f'' + + @property + def destination(self): + ''' + The IP address of the host to which the message should be sent. + + ''' + return self._destination + + @property + def id(self): + ''' + The identifier of the request. + Used to match the reply with the request. + + ''' + return self._id + + @property + def sequence(self): + ''' + The sequence number. + Used to match the reply with the request. + + ''' + return self._sequence + + @property + def payload(self): + ''' + The payload content in bytes. + Return a random payload if not defined. + + ''' + return self._payload or random_byte_message(self._payload_size) + + @property + def payload_size(self): + ''' + The payload size. + + ''' + return self._payload_size + + @property + def ttl(self): + ''' + The time to live of the packet in terms of hops. + + ''' + return self._ttl + + @property + def traffic_class(self): + ''' + The traffic class of the packet. + + ''' + return self._traffic_class + + @property + def time(self): + ''' + The timestamp of the ICMP request. + + Initialized to zero when creating the request and replaced by + the `send` method of an ICMP socket with the time of sending. + + ''' + return self._time + + +class ICMPReply: + ''' + A class that represents an ICMP reply. Generated from an ICMP socket. + + :type source: str + :param source: The IP address of the host that composes the ICMP + message. + + :type family: int + :param family: The address family. Can be set to `4` for IPv4 or `6` + for IPv6 addresses. + + :type id: int + :param id: The identifier of the reply. Used to match the reply with + the request. + + :type sequence: int + :param sequence: The sequence number. Used to match the reply with + the request. + + :type type: int + :param type: The type of ICMP message. + + :type code: int + :param code: The ICMP error code. + + :type bytes_received: int + :param bytes_received: The number of bytes received. + + :type time: float + :param time: The timestamp of the ICMP reply. + + ''' + __slots__ = '_source', '_family', '_id', '_sequence', '_type', \ + '_code', '_bytes_received', '_time' + + def __init__(self, source, family, id, sequence, type, code, + bytes_received, time): + + self._source = source + self._family = family + self._id = id + self._sequence = sequence + self._type = type + self._code = code + self._bytes_received = bytes_received + self._time = time + + def __repr__(self): + return f'' + + def raise_for_status(self): + ''' + Throw an exception if the reply is not an ICMP Echo Reply. + Otherwise, do nothing. + + :raises DestinationUnreachable: If the destination is + unreachable for some reason. + :raises TimeExceeded: If the time to live field of the ICMP + request has reached zero. + :raises ICMPError: Raised for any other type and ICMP error + code, except ICMP Echo Reply messages. + + ''' + if self._family == 6: + if self._type == 1: + raise ICMPv6DestinationUnreachable(self) + + if self._type == 3: + raise ICMPv6TimeExceeded(self) + else: + if self._type == 3: + raise ICMPv4DestinationUnreachable(self) + + if self._type == 11: + raise ICMPv4TimeExceeded(self) + + if (self._family == 4 and self._type != 0 or + self._family == 6 and self._type != 129): + message = f'Error type: {self._type}, code: {self._code}' + raise ICMPError(message, self) + + @property + def source(self): + ''' + The IP address of the host that composes the ICMP message. + + ''' + return self._source + + @property + def id(self): + ''' + The identifier of the reply. + Used to match the reply with the request. + + ''' + return self._id + + @property + def sequence(self): + ''' + The sequence number. + Used to match the reply with the request. + + ''' + return self._sequence + + @property + def type(self): + ''' + The type of ICMP message. + + ''' + return self._type + + @property + def code(self): + ''' + The ICMP error code. + + ''' + return self._code + + @property + def bytes_received(self): + ''' + The number of bytes received. + + ''' + return self._bytes_received + + @property + def time(self): + ''' + The timestamp of the ICMP reply. + + ''' + return self._time + + +class Host: + ''' + A class that represents a host. It simplifies the use of the results + from the `ping`, `multiping` and `traceroute` functions. + + :type address: str + :param address: The IP address of the host that responded to the + request. + + :type packets_sent: int + :param packets_sent: The number of packets transmitted to the + destination host. + + :type rtts: list[float] + :param rtts: The list of round-trip times expressed in milliseconds. + + ''' + __slots__ = '_address', '_packets_sent', '_rtts' + + def __init__(self, address, packets_sent, rtts): + self._address = address + self._packets_sent = packets_sent + self._rtts = rtts + + def __repr__(self): + return f'' + + def __str__(self): + return f' {self._address}\n' + '-' * 60 + '\n' \ + f' Packets sent: {self._packets_sent}\n' \ + f' Packets received: {self.packets_received}\n' \ + f' Packet loss: {self.packet_loss * 100}%\n' \ + f' Round-trip times: {self.min_rtt} ms / ' \ + f'{self.avg_rtt} ms / {self.max_rtt} ms\n' \ + f' Jitter: {self.jitter} ms\n' + '-' * 60 + + @property + def address(self): + ''' + The IP address of the host that responded to the request. + + ''' + return self._address + + @property + def min_rtt(self): + ''' + The minimum round-trip time in milliseconds. + + ''' + if not self._rtts: + return 0.0 + + return round(min(self._rtts), 3) + + @property + def avg_rtt(self): + ''' + The average round-trip time in milliseconds. + + ''' + if not self._rtts: + return 0.0 + + return round(sum(self._rtts) / len(self._rtts), 3) + + @property + def max_rtt(self): + ''' + The maximum round-trip time in milliseconds. + + ''' + if not self._rtts: + return 0.0 + + return round(max(self._rtts), 3) + + @property + def rtts(self): + ''' + The list of round-trip times expressed in milliseconds. + + ''' + return self._rtts + + @property + def packets_sent(self): + ''' + The number of requests transmitted to the remote host. + + ''' + return self._packets_sent + + @property + def packets_received(self): + ''' + The number of ICMP responses received from the remote host. + + ''' + return len(self._rtts) + + @property + def packet_loss(self): + ''' + Packet loss occurs when packets fail to reach their destination. + Return a `float` between 0 and 1 (all packets are lost). + + ''' + if not self._packets_sent: + return 0.0 + + return round(1 - len(self._rtts) / self._packets_sent, 2) + + @property + def jitter(self): + ''' + The jitter in milliseconds, defined as the variance of the + latency of packets flowing through the network. + + At least two ICMP responses are required to calculate the + jitter. + + ''' + sum_deltas = 0.0 + num_deltas = len(self._rtts) - 1 + + if num_deltas < 1: + return 0.0 + + for i in range(num_deltas): + sum_deltas += abs(self._rtts[i] - self._rtts[i + 1]) + + return round(sum_deltas / num_deltas, 3) + + @property + def is_alive(self): + ''' + Indicate whether the host is reachable. + Return a `boolean`. + + ''' + return len(self._rtts) > 0 + + +class Hop(Host): + ''' + A class that represents a hop. It extends the `Host` class and adds + some features for the `traceroute` function. + + :type address: str + :param address: The IP address of the gateway or host that responded + to the request. + + :type packets_sent: int + :param packets_sent: The number of packets transmitted to the + destination host. + + :type rtts: list[float] + :param rtts: The list of round-trip times expressed in milliseconds. + + :type distance: int + :param distance: The distance, in terms of hops, that separates the + remote host from the current machine. + + ''' + __slots__ = '_address', '_packets_sent', '_rtts', '_distance' + + def __init__(self, address, packets_sent, rtts, distance): + super().__init__(address, packets_sent, rtts) + self._distance = distance + + def __repr__(self): + return f'' + + def __str__(self): + return f' #{self._distance:<2} {super().__str__()[2:]}' + + @property + def distance(self): + ''' + The distance, in terms of hops, that separates the remote host + from the current machine. + + ''' + return self._distance diff --git a/script.plexmod/lib/_included_packages/icmplib/multiping.py b/script.plexmod/lib/_included_packages/icmplib/multiping.py new file mode 100644 index 000000000..0705b281d --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/multiping.py @@ -0,0 +1,277 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + +import asyncio + +from .ping import async_ping + + +async def async_multiping(addresses, count=2, interval=0.5, timeout=2, + concurrent_tasks=50, source=None, family=None, privileged=True, + **kwargs): + ''' + Send ICMP Echo Request packets to several network hosts. + + This function is non-blocking. + + :type addresses: list[str] + :param addresses: The IP addresses of the hosts to which messages + should be sent. Hostnames and FQDNs are allowed but not + recommended. You can easily retrieve their IP address by calling + the built-in `async_resolve` function. + + :type count: int, optional + :param count: The number of ping to perform per address. + Default to 2. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each packet. + Default to 0.5. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving a reply in + seconds. Default to 2. + + :type concurrent_tasks: int, optional + :param concurrent_tasks: The maximum number of concurrent tasks to + speed up processing. This value cannot exceed the maximum number + of file descriptors configured on the operating system. + Default to 50. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destinations. This parameter should not be used if + you are passing both IPv4 and IPv6 addresses to this function. + + :type family: int, optional + :param family: The address family if a hostname or FQDN is specified. + Can be set to `4` for IPv4 or `6` for IPv6 addresses. By default, + this function searches for IPv4 addresses first before searching + for IPv6 addresses. + + :type privileged: bool, optional + :param privileged: When this option is enabled, this library fully + manages the exchanges and the structure of ICMP packets. + Disable this option if you want to use this function without + root privileges and let the kernel handle ICMP headers. + Default to True. + Only available on Unix systems. Ignored on Windows. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. + Provides a defined level of service to packets by setting the DS + Field (formerly TOS) or the Traffic Class field of IP headers. + Packets are delivered with the minimum priority by default + (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. + + :rtype: list[Host] + :returns: A list of `Host` objects containing statistics about the + desired destinations. The list is sorted in the same order as + the addresses passed in parameters. + + :raises NameLookupError: If you pass a hostname or FQDN in + parameters and it does not exist or cannot be resolved. + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the source address cannot be assigned + to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. + + Usage:: + + >>> import asyncio + >>> from icmplib import async_multiping + >>> hosts = asyncio.run(async_multiping(['10.0.0.5', '::1'])) + + >>> for host in hosts: + ... if host.is_alive: + ... print(f'{host.address} is up!') + ... else: + ... print(f'{host.address} is down!') + + 10.0.0.5 is down! + ::1 is up! + + See the `Host` class for details. + + ''' + loop = asyncio.get_running_loop() + tasks = [] + tasks_pending = set() + + for address in addresses: + if len(tasks_pending) >= concurrent_tasks: + _, tasks_pending = await asyncio.wait( + tasks_pending, + return_when=asyncio.FIRST_COMPLETED) + + task = loop.create_task( + async_ping( + address=address, + count=count, + interval=interval, + timeout=timeout, + source=source, + family=family, + privileged=privileged, + **kwargs)) + + tasks.append(task) + tasks_pending.add(task) + + await asyncio.wait(tasks_pending) + + return [task.result() for task in tasks] + + +def multiping(addresses, count=2, interval=0.5, timeout=2, + concurrent_tasks=50, source=None, family=None, privileged=True, + **kwargs): + ''' + Send ICMP Echo Request packets to several network hosts. + + :type addresses: list[str] + :param addresses: The IP addresses of the hosts to which messages + should be sent. Hostnames and FQDNs are allowed but not + recommended. You can easily retrieve their IP address by calling + the built-in `resolve` function. + + :type count: int, optional + :param count: The number of ping to perform per address. + Default to 2. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each packet. + Default to 0.5. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving a reply in + seconds. Default to 2. + + :type concurrent_tasks: int, optional + :param concurrent_tasks: The maximum number of concurrent tasks to + speed up processing. This value cannot exceed the maximum number + of file descriptors configured on the operating system. + Default to 50. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destinations. This parameter should not be used if + you are passing both IPv4 and IPv6 addresses to this function. + + :type family: int, optional + :param family: The address family if a hostname or FQDN is specified. + Can be set to `4` for IPv4 or `6` for IPv6 addresses. By default, + this function searches for IPv4 addresses first before searching + for IPv6 addresses. + + :type privileged: bool, optional + :param privileged: When this option is enabled, this library fully + manages the exchanges and the structure of ICMP packets. + Disable this option if you want to use this function without + root privileges and let the kernel handle ICMP headers. + Default to True. + Only available on Unix systems. Ignored on Windows. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. + Provides a defined level of service to packets by setting the DS + Field (formerly TOS) or the Traffic Class field of IP headers. + Packets are delivered with the minimum priority by default + (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. + + :rtype: list[Host] + :returns: A list of `Host` objects containing statistics about the + desired destinations. The list is sorted in the same order as + the addresses passed in parameters. + + :raises NameLookupError: If you pass a hostname or FQDN in + parameters and it does not exist or cannot be resolved. + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the source address cannot be assigned + to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. + + Usage:: + + >>> from icmplib import multiping + >>> hosts = multiping(['10.0.0.5', '127.0.0.1', '::1']) + + >>> for host in hosts: + ... if host.is_alive: + ... print(f'{host.address} is up!') + ... else: + ... print(f'{host.address} is down!') + + 10.0.0.5 is down! + 127.0.0.1 is up! + ::1 is up! + + See the `Host` class for details. + + ''' + return asyncio.run( + async_multiping( + addresses=addresses, + count=count, + interval=interval, + timeout=timeout, + concurrent_tasks=concurrent_tasks, + source=source, + family=family, + privileged=privileged, + **kwargs)) diff --git a/script.plexmod/lib/_included_packages/icmplib/ping.py b/script.plexmod/lib/_included_packages/icmplib/ping.py new file mode 100644 index 000000000..5ba1efc8b --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/ping.py @@ -0,0 +1,298 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + +import asyncio +from time import sleep + +from .sockets import ICMPv4Socket, ICMPv6Socket, AsyncSocket +from .models import ICMPRequest, Host +from .exceptions import ICMPLibError +from .utils import * + + +def ping(address, count=4, interval=1, timeout=2, id=None, source=None, + family=None, privileged=True, **kwargs): + ''' + Send ICMP Echo Request packets to a network host. + + :type address: str + :param address: The IP address, hostname or FQDN of the host to + which messages should be sent. For deterministic behavior, + prefer to use an IP address. + + :type count: int, optional + :param count: The number of ping to perform. Default to 4. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each packet. + Default to 1. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving a reply in + seconds. Default to 2. + + :type id: int, optional + :param id: The identifier of ICMP requests. Used to match the + responses with requests. In practice, a unique identifier should + be used for every ping process. On Linux, this identifier is + ignored when the `privileged` parameter is disabled. The library + handles this identifier itself by default. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destination. + + :type family: int, optional + :param family: The address family if a hostname or FQDN is specified. + Can be set to `4` for IPv4 or `6` for IPv6 addresses. By default, + this function searches for IPv4 addresses first before searching + for IPv6 addresses. + + :type privileged: bool, optional + :param privileged: When this option is enabled, this library fully + manages the exchanges and the structure of ICMP packets. + Disable this option if you want to use this function without + root privileges and let the kernel handle ICMP headers. + Default to True. + Only available on Unix systems. Ignored on Windows. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. + Provides a defined level of service to packets by setting the DS + Field (formerly TOS) or the Traffic Class field of IP headers. + Packets are delivered with the minimum priority by default + (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. + + :rtype: Host + :returns: A `Host` object containing statistics about the desired + destination. + + :raises NameLookupError: If you pass a hostname or FQDN in + parameters and it does not exist or cannot be resolved. + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the source address cannot be assigned + to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. + + Usage:: + + >>> from icmplib import ping + >>> host = ping('1.1.1.1') + >>> host.avg_rtt + 13.2 + >>> host.is_alive + True + + See the `Host` class for details. + + ''' + if is_hostname(address): + address = resolve(address, family)[0] + + if is_ipv6_address(address): + _Socket = ICMPv6Socket + else: + _Socket = ICMPv4Socket + + id = id or unique_identifier() + packets_sent = 0 + rtts = [] + + with _Socket(source, privileged) as sock: + for sequence in range(count): + if sequence > 0: + sleep(interval) + + request = ICMPRequest( + destination=address, + id=id, + sequence=sequence, + **kwargs) + + try: + sock.send(request) + packets_sent += 1 + + reply = sock.receive(request, timeout) + reply.raise_for_status() + + rtt = (reply.time - request.time) * 1000 + rtts.append(rtt) + + except ICMPLibError: + pass + + return Host(address, packets_sent, rtts) + + +async def async_ping(address, count=4, interval=1, timeout=2, id=None, + source=None, family=None, privileged=True, **kwargs): + ''' + Send ICMP Echo Request packets to a network host. + + This function is non-blocking. + + :type address: str + :param address: The IP address, hostname or FQDN of the host to + which messages should be sent. For deterministic behavior, + prefer to use an IP address. + + :type count: int, optional + :param count: The number of ping to perform. Default to 4. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each packet. + Default to 1. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving a reply in + seconds. Default to 2. + + :type id: int, optional + :param id: The identifier of ICMP requests. Used to match the + responses with requests. In practice, a unique identifier should + be used for every ping process. On Linux, this identifier is + ignored when the `privileged` parameter is disabled. The library + handles this identifier itself by default. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destination. + + :type family: int, optional + :param family: The address family if a hostname or FQDN is specified. + Can be set to `4` for IPv4 or `6` for IPv6 addresses. By default, + this function searches for IPv4 addresses first before searching + for IPv6 addresses. + + :type privileged: bool, optional + :param privileged: When this option is enabled, this library fully + manages the exchanges and the structure of ICMP packets. + Disable this option if you want to use this function without + root privileges and let the kernel handle ICMP headers. + Default to True. + Only available on Unix systems. Ignored on Windows. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. + Provides a defined level of service to packets by setting the DS + Field (formerly TOS) or the Traffic Class field of IP headers. + Packets are delivered with the minimum priority by default + (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. + + :rtype: Host + :returns: A `Host` object containing statistics about the desired + destination. + + :raises NameLookupError: If you pass a hostname or FQDN in + parameters and it does not exist or cannot be resolved. + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the source address cannot be assigned + to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. + + Usage:: + + >>> import asyncio + >>> from icmplib import async_ping + >>> host = asyncio.run(async_ping('1.1.1.1')) + >>> host.avg_rtt + 13.2 + >>> host.is_alive + True + + See the `Host` class for details. + + ''' + if is_hostname(address): + address = (await async_resolve(address, family))[0] + + if is_ipv6_address(address): + _Socket = ICMPv6Socket + else: + _Socket = ICMPv4Socket + + id = id or unique_identifier() + packets_sent = 0 + rtts = [] + + with AsyncSocket(_Socket(source, privileged)) as sock: + for sequence in range(count): + if sequence > 0: + await asyncio.sleep(interval) + + request = ICMPRequest( + destination=address, + id=id, + sequence=sequence, + **kwargs) + + try: + sock.send(request) + packets_sent += 1 + + reply = await sock.receive(request, timeout) + reply.raise_for_status() + + rtt = (reply.time - request.time) * 1000 + rtts.append(rtt) + + except ICMPLibError: + pass + + return Host(address, packets_sent, rtts) diff --git a/script.plexmod/lib/_included_packages/icmplib/sockets.py b/script.plexmod/lib/_included_packages/icmplib/sockets.py new file mode 100644 index 000000000..130f55a3a --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/sockets.py @@ -0,0 +1,806 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + +import socket, asyncio +from struct import pack, unpack +from time import time + +from .models import ICMPReply +from .exceptions import * +from .utils import PLATFORM_LINUX, PLATFORM_MACOS, PLATFORM_WINDOWS + + +class ICMPSocket: + ''' + Base class for ICMP sockets. + + :type address: str, optional + :param address: The IP address from which you want to listen and + send packets. By default, the socket listens on all interfaces. + + :type privileged: bool, optional + :param privileged: When this option is enabled, the socket fully + manages the exchanges and the structure of the ICMP packets. + Disable this option if you want to instantiate and use the + socket without root privileges and let the kernel handle ICMP + headers. Default to True. + Only available on Unix systems. Ignored on Windows. + + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the requested address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs while creating the + socket. + + ''' + __slots__ = '_sock', '_address', '_privileged' + + _IP_VERSION = -1 + _ICMP_HEADER_OFFSET = -1 + _ICMP_HEADER_REAL_OFFSET = -1 + + _ICMP_CODE_OFFSET = _ICMP_HEADER_OFFSET + 1 + _ICMP_CHECKSUM_OFFSET = _ICMP_HEADER_OFFSET + 2 + _ICMP_ID_OFFSET = _ICMP_HEADER_OFFSET + 4 + _ICMP_SEQUENCE_OFFSET = _ICMP_HEADER_OFFSET + 6 + _ICMP_PAYLOAD_OFFSET = _ICMP_HEADER_OFFSET + 8 + + _ICMP_ECHO_REQUEST = -1 + _ICMP_ECHO_REPLY = -1 + + def __init__(self, address=None, privileged=True): + self._sock = None + self._address = address + + # The Linux kernel allows unprivileged users to use datagram + # sockets (SOCK_DGRAM) to send ICMP requests. This feature is + # now supported by the majority of Unix systems. + # Windows is not compatible. + self._privileged = privileged or PLATFORM_WINDOWS + + try: + self._sock = self._create_socket( + socket.SOCK_RAW if self._privileged else + socket.SOCK_DGRAM) + + if address: + self._sock.bind((address, 0)) + + except OSError as err: + if err.errno in (1, 13, 10013): + raise SocketPermissionError(privileged) + + if err.errno in (-9, 49, 99, 10049, 11001): + raise SocketAddressError(address) + + raise ICMPSocketError(str(err)) + + def __enter__(self): + ''' + Return this object. + + ''' + return self + + def __exit__(self, type, value, traceback): + ''' + Call the `close` method. + + ''' + self.close() + + def __del__(self): + ''' + Call the `close` method. + + ''' + self.close() + + def _create_socket(self, type): + ''' + Create and return a new socket. Must be overridden. + + ''' + raise NotImplementedError + + def _set_ttl(self, ttl): + ''' + Set the time to live of every IP packet originating from this + socket. Must be overridden. + + ''' + raise NotImplementedError + + def _set_traffic_class(self, traffic_class): + ''' + Set the DS Field (formerly TOS) or the Traffic Class field of + every IP packet originating from this socket. Must be + overridden. + + ''' + raise NotImplementedError + + def _checksum(self, data): + ''' + Compute the checksum of an ICMP packet. Checksums are used to + verify the integrity of packets. + + ''' + sum = 0 + data += b'\x00' + + for i in range(0, len(data) - 1, 2): + sum += (data[i] << 8) + data[i + 1] + sum = (sum & 0xffff) + (sum >> 16) + + sum = ~sum & 0xffff + + return sum + + def _create_packet(self, id, sequence, payload): + ''' + Build an ICMP packet from an identifier, a sequence number and + a payload. + + This method returns the newly created ICMP header concatenated + to the payload passed in parameters. + + ''' + checksum = 0 + + # Temporary ICMP header to compute the checksum + header = pack('!2B3H', self._ICMP_ECHO_REQUEST, 0, checksum, + id, sequence) + + checksum = self._checksum(header + payload) + + # Definitive ICMP header + header = pack('!2B3H', self._ICMP_ECHO_REQUEST, 0, checksum, + id, sequence) + + return header + payload + + def _parse_reply(self, packet, source, current_time): + ''' + Parse an ICMP reply from bytes. + + This method returns an `ICMPReply` object or `None` if the reply + cannot be parsed. + + ''' + # On Linux, the IP header is missing when a datagram socket is + # used (SOCK_DGRAM). To keep the same behavior on all operating + # systems including macOS which has this header, we add a + # padding of the size of the missing IP header. + if not self._privileged and PLATFORM_LINUX: + packet = b'\x00' * self._ICMP_HEADER_OFFSET + packet + + bytes_received = len(packet) - self._ICMP_HEADER_OFFSET + + if len(packet) < self._ICMP_CHECKSUM_OFFSET: + return None + + type, code = unpack('!2B', packet[ + self._ICMP_HEADER_OFFSET: + self._ICMP_CHECKSUM_OFFSET]) + + if type != self._ICMP_ECHO_REPLY: + packet = packet[ + self._ICMP_PAYLOAD_OFFSET + - self._ICMP_HEADER_OFFSET + + self._ICMP_HEADER_REAL_OFFSET:] + + if len(packet) < self._ICMP_PAYLOAD_OFFSET: + return None + + id, sequence = unpack('!2H', packet[ + self._ICMP_ID_OFFSET: + self._ICMP_PAYLOAD_OFFSET]) + + return ICMPReply( + source=source, + family=self._IP_VERSION, + id=id, + sequence=sequence, + type=type, + code=code, + bytes_received=bytes_received, + time=current_time) + + def send(self, request): + ''' + Send an ICMP request message over the network to a remote host. + + This operation is non-blocking. Use the `receive` method to get + the reply. + + :type request: ICMPRequest + :param request: The ICMP request you have created. If the socket + is used in non-privileged mode on a Linux system, the + identifier defined in the request will be replaced by the + kernel. + + :raises SocketBroadcastError: If a broadcast address is used and + the corresponding option is not enabled on the socket + (ICMPv4 only). + :raises SocketUnavailableError: If the socket is closed. + :raises ICMPSocketError: If another error occurs while sending. + + ''' + if not self._sock: + raise SocketUnavailableError + + try: + sock_destination = socket.getaddrinfo( + host=request.destination, + port=None, + family=self._sock.family, + type=self._sock.type)[0][4] + + packet = self._create_packet( + id=request.id, + sequence=request.sequence, + payload=request.payload) + + self._set_ttl(request.ttl) + self._set_traffic_class(request.traffic_class) + + request._time = time() + self._sock.sendto(packet, sock_destination) + + # On Linux, the ICMP request identifier is replaced by the + # kernel with a random port number when a datagram socket is + # used (SOCK_DGRAM). So, we update the request created by + # the user to take this new identifier into account. + if not self._privileged and PLATFORM_LINUX: + request._id = self._sock.getsockname()[1] + + except PermissionError: + raise SocketBroadcastError + + except OSError as err: + raise ICMPSocketError(str(err)) + + def receive(self, request=None, timeout=2): + ''' + Receive an ICMP reply message from the socket. + + This method can be called multiple times if you expect several + responses as with a broadcast address. + + :type request: ICMPRequest, optional + :param request: The ICMP request to use to match the response. + By default, all ICMP packets arriving on the socket are + returned. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving the + response in seconds. Default to 2. + + :rtype: ICMPReply + :returns: An `ICMPReply` object representing the response of the + desired destination or an upstream gateway. See the + `ICMPReply` class for details. + + :raises TimeoutExceeded: If no response is received before the + timeout specified in parameters. + :raises SocketUnavailableError: If the socket is closed. + :raises ICMPSocketError: If another error occurs while receiving. + + ''' + if not self._sock: + raise SocketUnavailableError + + self._sock.settimeout(timeout) + time_limit = time() + timeout + + try: + while True: + response = self._sock.recvfrom(1024) + current_time = time() + + packet = response[0] + source = response[1][0] + + if current_time > time_limit: + raise socket.timeout + + reply = self._parse_reply( + packet=packet, + source=source, + current_time=current_time) + + if (reply and not request or + reply and request.id == reply.id and + request.sequence == reply.sequence): + return reply + + except socket.timeout: + raise TimeoutExceeded(timeout) + + except OSError as err: + raise ICMPSocketError(str(err)) + + def close(self): + ''' + Close the socket. It cannot be used after this call. + + ''' + if self._sock: + self._sock.close() + self._sock = None + + @property + def sock(self): + ''' + Return the underlying socket (`socket.socket` object) or `None` + if the socket is closed. + + This property should only be used if the feature you want is not + yet implemented. Some changes made to this socket may cause + unexpected behavior or be incompatible with later versions of + the library. + + Prefer to use the other methods and properties defined within + this class if possible. + + ''' + return self._sock + + @property + def blocking(self): + ''' + Indicate whether the socket is in blocking mode. + Return a `boolean`. + + ''' + return self._sock.getblocking() + + @blocking.setter + def blocking(self, enable): + return self._sock.setblocking(enable) + + @property + def address(self): + ''' + The IP address from which the socket listens and sends packets. + Return `None` if the socket listens on all interfaces. + + ''' + return self._address + + @property + def is_privileged(self): + ''' + Indicate whether the socket is running in privileged mode. + Return a `boolean`. + + ''' + return self._privileged + + @property + def is_closed(self): + ''' + Indicate whether the socket is closed. + Return a `boolean`. + + ''' + return self._sock is None + + +# Echo Request and Echo Reply messages RFC 792 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Identifier | Sequence Number | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Data ... +# +-+-+-+-+- +# +# ICMPv4 Error message RFC 792 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Unused / Depends on the error | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Internet Header + 64 bits of Original Data Datagram | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +class ICMPv4Socket(ICMPSocket): + ''' + Class for sending and receiving ICMPv4 packets. + + :type address: str, optional + :param address: The IP address from which you want to listen and + send packets. By default, the socket listens on all interfaces. + + :type privileged: bool, optional + :param privileged: When this option is enabled, the socket fully + manages the exchanges and the structure of the ICMP packets. + Disable this option if you want to instantiate and use the + socket without root privileges and let the kernel handle ICMP + headers. Default to True. + Only available on Unix systems. Ignored on Windows. + + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the requested address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs while creating the + socket. + + ''' + __slots__ = '_sock', '_address', '_privileged' + + _IP_VERSION = 4 + _ICMP_HEADER_OFFSET = 20 + _ICMP_HEADER_REAL_OFFSET = 20 + + _ICMP_CODE_OFFSET = _ICMP_HEADER_OFFSET + 1 + _ICMP_CHECKSUM_OFFSET = _ICMP_HEADER_OFFSET + 2 + _ICMP_ID_OFFSET = _ICMP_HEADER_OFFSET + 4 + _ICMP_SEQUENCE_OFFSET = _ICMP_HEADER_OFFSET + 6 + _ICMP_PAYLOAD_OFFSET = _ICMP_HEADER_OFFSET + 8 + + _ICMP_ECHO_REQUEST = 8 + _ICMP_ECHO_REPLY = 0 + + def _create_socket(self, type): + ''' + Create and return a new socket. + + ''' + return socket.socket( + family=socket.AF_INET, + type=type, + proto=socket.IPPROTO_ICMP) + + def _set_ttl(self, ttl): + ''' + Set the time to live of every IP packet originating from this + socket. + + ''' + self._sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_TTL, + ttl) + + def _set_traffic_class(self, traffic_class): + ''' + Set the DS Field (formerly TOS) of every IP packet originating + from this socket. + + Only available on Unix systems. Ignored on Windows. + + ''' + # Not available on Windows + if PLATFORM_WINDOWS: + return + + self._sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_TOS, + traffic_class) + + @property + def broadcast(self): + ''' + Indicate whether broadcast support is enabled on the socket. + Return a `boolean`. + + .. note:: + To enable broadcast support: + icmp_socket.broadcast = True + + ''' + return self._sock.getsockopt( + socket.SOL_SOCKET, + socket.SO_BROADCAST) > 0 + + @broadcast.setter + def broadcast(self, enable): + self._sock.setsockopt( + socket.SOL_SOCKET, + socket.SO_BROADCAST, + enable) + + +# Echo Request and Echo Reply messages RFC 4443 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Identifier | Sequence Number | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Data ... +# +-+-+-+-+- +# +# ICMPv6 Error message RFC 4443 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Unused / Depends on the error | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Original packet without exceed the minimum IPv6 MTU | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +# Windows IPv6 compatibility +if PLATFORM_WINDOWS: + socket.IPPROTO_IPV6 = 41 + socket.IPPROTO_ICMPV6 = 58 + + +class ICMPv6Socket(ICMPSocket): + ''' + Class for sending and receiving ICMPv6 packets. + + :type address: str, optional + :param address: The IP address from which you want to listen and + send packets. By default, the socket listens on all interfaces. + + :type privileged: bool, optional + :param privileged: When this option is enabled, the socket fully + manages the exchanges and the structure of the ICMP packets. + Disable this option if you want to instantiate and use the + socket without root privileges and let the kernel handle ICMP + headers. Default to True. + Only available on Unix systems. Ignored on Windows. + + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the requested address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs while creating the + socket. + + ''' + __slots__ = '_sock', '_address', '_privileged' + + _IP_VERSION = 6 + _ICMP_HEADER_OFFSET = 0 + _ICMP_HEADER_REAL_OFFSET = 40 + + _ICMP_CODE_OFFSET = _ICMP_HEADER_OFFSET + 1 + _ICMP_CHECKSUM_OFFSET = _ICMP_HEADER_OFFSET + 2 + _ICMP_ID_OFFSET = _ICMP_HEADER_OFFSET + 4 + _ICMP_SEQUENCE_OFFSET = _ICMP_HEADER_OFFSET + 6 + _ICMP_PAYLOAD_OFFSET = _ICMP_HEADER_OFFSET + 8 + + _ICMP_ECHO_REQUEST = 128 + _ICMP_ECHO_REPLY = 129 + + def _create_socket(self, type): + ''' + Create and return a new socket. + + ''' + return socket.socket( + family=socket.AF_INET6, + type=type, + proto=socket.IPPROTO_ICMPV6) + + def _set_ttl(self, ttl): + ''' + Set the time to live of every IP packet originating from this + socket. + + ''' + # Not available on macOS when the privileged param. is disabled + if PLATFORM_MACOS and not self._privileged: + return + + self._sock.setsockopt( + socket.IPPROTO_IPV6, + socket.IPV6_UNICAST_HOPS, + ttl) + + def _set_traffic_class(self, traffic_class): + ''' + Set the Traffic Class field of every IP packet originating from + this socket. + + Only available on Unix systems. Ignored on Windows. + + ''' + # Not available on Windows + if PLATFORM_WINDOWS: + return + + # Not available on macOS when the privileged param. is disabled + if PLATFORM_MACOS and not self._privileged: + return + + self._sock.setsockopt( + socket.IPPROTO_IPV6, + socket.IPV6_TCLASS, + traffic_class) + + +class AsyncSocket: + ''' + A wrapper for ICMP sockets which makes them asynchronous. + + :type icmp_sock: ICMPSocket + :param icmp_sock: An ICMP socket. Once the wrapper is instantiated, + this socket should no longer be used directly. + + ''' + __slots__ = '_icmp_sock' + + def __init__(self, icmp_sock): + self._icmp_sock = icmp_sock + self._icmp_sock.blocking = False + + def __getattr__(self, name): + ''' + Return the specified attribute of the underlying ICMP socket. + + ''' + if not self._icmp_sock: + raise SocketUnavailableError + + return getattr(self._icmp_sock, name) + + def __enter__(self): + ''' + Return this object. + + ''' + return self + + def __exit__(self, type, value, traceback): + ''' + Call the `close` method. + + ''' + self.close() + + def __del__(self): + ''' + Call the `close` method. + + ''' + self.close() + + async def receive(self, request=None, timeout=2): + ''' + Receive an ICMP reply message from the socket. + + This method can be called multiple times if you expect several + responses as with a broadcast address. + + This method is non-blocking. + + :type request: ICMPRequest, optional + :param request: The ICMP request to use to match the response. + By default, all ICMP packets arriving on the socket are + returned. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving the + response in seconds. Default to 2. + + :rtype: ICMPReply + :returns: An `ICMPReply` object representing the response of the + desired destination or an upstream gateway. See the + `ICMPReply` class for details. + Unlike the `reveive` method of synchronous ICMP sockets, the + returned `ICMPReply` object does not specify the IP address + of the host that replied to the request message. + + :raises TimeoutExceeded: If no response is received before the + timeout specified in parameters. + :raises SocketUnavailableError: If the socket is closed. + :raises ICMPSocketError: If another error occurs while receiving. + + ''' + if not self._icmp_sock or not self._icmp_sock._sock: + raise SocketUnavailableError + + loop = asyncio.get_running_loop() + time_limit = time() + timeout + remaining_time = timeout + + try: + while True: + packet = await asyncio.wait_for( + loop.sock_recv(self._icmp_sock._sock, 1024), + remaining_time) + + current_time = time() + + if current_time > time_limit: + raise asyncio.TimeoutError + + reply = self._parse_reply( + packet=packet, + source=None, + current_time=current_time) + + if (reply and not request or + reply and request.id == reply.id and + request.sequence == reply.sequence): + return reply + + remaining_time = time_limit - current_time + + except asyncio.TimeoutError: + raise TimeoutExceeded(timeout) + + except OSError as err: + raise ICMPSocketError(str(err)) + + finally: + if isinstance(loop, asyncio.SelectorEventLoop): + loop.remove_reader(self._icmp_sock._sock) + + def detach(self): + ''' + Detach the socket from the wrapper and return it. The wrapper + cannot be used after this call but the socket can be reused for + other purposes. + + ''' + icmp_sock = self._icmp_sock + + if self._icmp_sock: + self._icmp_sock = None + + return icmp_sock + + def close(self): + ''' + Detach the underlying socket from the wrapper and close it. Both + cannot be used after this call. + + ''' + if self._icmp_sock: + self.detach().close() + + @property + def is_closed(self): + ''' + Indicate whether the underlying socket is closed or detached + from this wrapper. Return a `boolean`. + + ''' + return self._icmp_sock is None diff --git a/script.plexmod/lib/_included_packages/icmplib/traceroute.py b/script.plexmod/lib/_included_packages/icmplib/traceroute.py new file mode 100644 index 000000000..7dd45f876 --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/traceroute.py @@ -0,0 +1,212 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + +from time import sleep + +from .sockets import ICMPv4Socket, ICMPv6Socket +from .models import ICMPRequest, Hop +from .exceptions import TimeExceeded, ICMPLibError +from .utils import * + + +def traceroute(address, count=2, interval=0.05, timeout=2, first_hop=1, + max_hops=30, fast=False, id=None, source=None, family=None, + **kwargs): + ''' + Determine the route to a destination host. + + The Internet is a large and complex aggregation of network hardware, + connected together by gateways. Tracking the route one's packets + follow can be difficult. This function uses the IP protocol time to + live field and attempts to elicit an ICMP Time Exceeded response + from each gateway along the path to some host. + + This function requires root privileges to run. + + :type address: str + :param address: The IP address, hostname or FQDN of the host to + reach. For deterministic behavior, prefer to use an IP address. + + :type count: int, optional + :param count: The number of ping to perform per hop. Default to 2. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each packet. + Default to 0.05. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving a reply in + seconds. Default to 2. + + :type first_hop: int, optional + :param first_hop: The initial time to live value used in outgoing + probe packets. Default to 1. + + :type max_hops: int, optional + :param max_hops: The maximum time to live (max number of hops) used + in outgoing probe packets. Default to 30. + + :type fast: bool, optional + :param fast: When this option is enabled and an intermediate router + has been reached, skip to the next hop rather than perform + additional requests. The `count` parameter then becomes the + maximum number of requests in the event of no response. + Default to False. + + :type id: int, optional + :param id: The identifier of ICMP requests. Used to match the + responses with requests. In practice, a unique identifier should + be used for every traceroute process. The library handles this + identifier itself by default. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destination. + + :type family: int, optional + :param family: The address family if a hostname or FQDN is specified. + Can be set to `4` for IPv4 or `6` for IPv6 addresses. By default, + this function searches for IPv4 addresses first before searching + for IPv6 addresses. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. + Provides a defined level of service to packets by setting the DS + Field (formerly TOS) or the Traffic Class field of IP headers. + Packets are delivered with the minimum priority by default + (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. + + :rtype: list[Hop] + :returns: A list of `Hop` objects representing the route to the + desired destination. The list is sorted in ascending order + according to the distance, in terms of hops, that separates the + remote host from the current machine. Gateways that do not + respond to requests are not added to this list. + + :raises NameLookupError: If you pass a hostname or FQDN in + parameters and it does not exist or cannot be resolved. + :raises SocketPermissionError: If the privileges are insufficient to + create the socket. + :raises SocketAddressError: If the source address cannot be assigned + to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. + + Usage:: + + >>> from icmplib import traceroute + >>> hops = traceroute('1.1.1.1') + >>> last_distance = 0 + + >>> for hop in hops: + ... if last_distance + 1 != hop.distance: + ... print('Some gateways are not responding') + ... + ... print(f'{hop.distance} {hop.address} {hop.avg_rtt} ms') + ... + ... last_distance = hop.distance + + 1 10.0.0.1 5.196 ms + 2 194.149.169.49 7.552 ms + 3 194.149.166.54 12.21 ms + * Some gateways are not responding + 5 212.73.205.22 22.15 ms + 6 1.1.1.1 13.59 ms + + See the `Hop` class for details. + + ''' + if is_hostname(address): + address = resolve(address, family)[0] + + if is_ipv6_address(address): + _Socket = ICMPv6Socket + else: + _Socket = ICMPv4Socket + + id = id or unique_identifier() + ttl = first_hop + host_reached = False + hops = [] + + with _Socket(source) as sock: + while not host_reached and ttl <= max_hops: + reply = None + packets_sent = 0 + rtts = [] + + for sequence in range(count): + request = ICMPRequest( + destination=address, + id=id, + sequence=sequence, + ttl=ttl, + **kwargs) + + try: + sock.send(request) + packets_sent += 1 + + reply = sock.receive(request, timeout) + rtt = (reply.time - request.time) * 1000 + rtts.append(rtt) + + reply.raise_for_status() + host_reached = True + + except TimeExceeded: + sleep(interval) + + except ICMPLibError: + break + + if reply: + hop = Hop( + address=reply.source, + packets_sent=packets_sent, + rtts=rtts, + distance=ttl) + + hops.append(hop) + + ttl += 1 + + return hops diff --git a/script.plexmod/lib/_included_packages/icmplib/utils.py b/script.plexmod/lib/_included_packages/icmplib/utils.py new file mode 100644 index 000000000..00e2664db --- /dev/null +++ b/script.plexmod/lib/_included_packages/icmplib/utils.py @@ -0,0 +1,199 @@ +''' + icmplib + ~~~~~~~ + + Easily forge ICMP packets and make your own ping and traceroute. + + https://github.com/ValentinBELYN/icmplib + + :copyright: Copyright 2017-2023 Valentin BELYN. + :license: GNU LGPLv3, see the LICENSE for details. + + ~~~~~~~ + + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. If not, see + . +''' + +import socket, asyncio + +from threading import Lock +from sys import platform +from os import getpid +from re import match +from random import choices + +from .exceptions import NameLookupError + + +PID = getpid() +PLATFORM_LINUX = platform == 'linux' +PLATFORM_MACOS = platform == 'darwin' +PLATFORM_WINDOWS = platform == 'win32' + +_lock_id = Lock() +_current_id = PID + + +def random_byte_message(size): + ''' + Generate a random byte sequence of the specified size. + + ''' + sequence = choices( + b'abcdefghijklmnopqrstuvwxyz' + b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + b'1234567890', k=size) + + return bytes(sequence) + + +def unique_identifier(): + ''' + Generate a unique identifier between 0 and 65535. + The first number generated will be equal to the PID + 1. + + ''' + global _current_id + + with _lock_id: + _current_id += 1 + _current_id &= 0xffff + + return _current_id + + +def resolve(name, family=None): + ''' + Resolve a hostname or FQDN to an IP address. Depending on the name + specified in parameters, several IP addresses may be returned. + + This function relies on the DNS name server configured on your + operating system. + + :type name: str + :param name: A hostname or a Fully Qualified Domain Name (FQDN). + + :type family: int, optional + :param family: The address family. Can be set to `4` for IPv4 or `6` + for IPv6 addresses. By default, this function searches for IPv4 + addresses first for compatibility reasons (A DNS lookup) before + searching for IPv6 addresses (AAAA DNS lookup). + + :rtype: list[str] + :returns: A list of IP addresses corresponding to the name passed as + a parameter. + + :raises NameLookupError: If the requested name does not exist or + cannot be resolved. + + ''' + try: + if family == 6: + _family = socket.AF_INET6 + else: + _family = socket.AF_INET + + lookup = socket.getaddrinfo( + host=name, + port=None, + family=_family, + type=socket.SOCK_DGRAM) + + return [address[4][0] for address in lookup] + + except OSError: + if not family: + return resolve(name, 6) + + raise NameLookupError(name) + + +async def async_resolve(name, family=None): + ''' + Resolve a hostname or FQDN to an IP address. Depending on the name + specified in parameters, several IP addresses may be returned. + + This function relies on the DNS name server configured on your + operating system. + + This function is non-blocking. + + :type name: str + :param name: A hostname or a Fully Qualified Domain Name (FQDN). + + :type family: int, optional + :param family: The address family. Can be set to `4` for IPv4 or `6` + for IPv6 addresses. By default, this function searches for IPv4 + addresses first for compatibility reasons (A DNS lookup) before + searching for IPv6 addresses (AAAA DNS lookup). + + :rtype: list[str] + :returns: A list of IP addresses corresponding to the name passed as + a parameter. + + :raises NameLookupError: If the requested name does not exist or + cannot be resolved. + + ''' + try: + if family == 6: + _family = socket.AF_INET6 + else: + _family = socket.AF_INET + + loop = asyncio.get_running_loop() + + lookup = await loop.getaddrinfo( + host=name, + port=None, + family=_family, + type=socket.SOCK_DGRAM) + + return [address[4][0] for address in lookup] + + except OSError: + if not family: + return await async_resolve(name, 6) + + raise NameLookupError(name) + + +def is_hostname(name): + ''' + Indicate whether the specified name is a hostname or an FQDN. + Return a `boolean`. + + ''' + pattern = r'(?i)^([a-z0-9-]+|([a-z0-9_-]+[.])+[a-z]+)$' + return match(pattern, name) is not None + + +def is_ipv4_address(address): + ''' + Indicate whether the specified address is an IPv4 address. + Return a `boolean`. + + ''' + pattern = r'^([0-9]{1,3}[.]){3}[0-9]{1,3}$' + return match(pattern, address) is not None + + +def is_ipv6_address(address): + ''' + Indicate whether the specified address is an IPv6 address. + Return a `boolean`. + + ''' + return ':' in address diff --git a/script.copacetic.helper/resources/__init__.py b/script.plexmod/lib/_included_packages/plexnet/__init__.py similarity index 100% rename from script.copacetic.helper/resources/__init__.py rename to script.plexmod/lib/_included_packages/plexnet/__init__.py diff --git a/script.plexmod/lib/_included_packages/plexnet/asyncadapter.py b/script.plexmod/lib/_included_packages/plexnet/asyncadapter.py new file mode 100644 index 000000000..97fbcd741 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/asyncadapter.py @@ -0,0 +1,338 @@ +from __future__ import absolute_import +import time +import socket +import six + +import requests +import six + +from requests.packages.urllib3 import HTTPConnectionPool, HTTPSConnectionPool +from requests.packages.urllib3.connection import HTTPConnection +from requests.packages.urllib3.poolmanager import PoolManager, proxy_from_url +try: + from requests.packages.urllib3.connectionpool import VerifiedHTTPSConnection +except ImportError: + # urllib3 >= 2.1.0 + from requests.packages.urllib3.connection import HTTPSConnection as VerifiedHTTPSConnection + +from requests.adapters import HTTPAdapter +from requests.compat import urlparse + +#from six.moves.http_client import HTTPConnection +import errno + +DEFAULT_POOLBLOCK = False +SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs', + 'ssl_version') + +WIN_WSAEINVAL = 10022 +WIN_EWOULDBLOCK = 10035 +WIN_ECONNRESET = 10054 +WIN_EISCONN = 10056 +WIN_ENOTCONN = 10057 +WIN_EHOSTUNREACH = 10065 + + +def ABORT_FLAG_FUNCTION(): + return False + + +class TimeoutException(Exception): + pass + + +class CanceledException(Exception): + pass + + +class AsyncTimeout(float): + def __repr__(self): + return '{0}({1})'.format(float(self), self.getConnectTimeout()) + + def __str__(self): + return repr(self) + + @classmethod + def fromTimeout(cls, t): + if isinstance(t, AsyncTimeout): + return t + + try: + return AsyncTimeout(float(t)) or DEFAULT_TIMEOUT + except TypeError: + return DEFAULT_TIMEOUT + + def setConnectTimeout(self, val): + self._connectTimout = val + return self + + def getConnectTimeout(self): + if hasattr(self, '_connectTimout'): + return self._connectTimout + + return self + + +DEFAULT_TIMEOUT = AsyncTimeout(10).setConnectTimeout(10) + + +class AsyncVerifiedHTTPSConnection(VerifiedHTTPSConnection): + def __init__(self, *args, **kwargs): + VerifiedHTTPSConnection.__init__(self, *args, **kwargs) + self._canceled = False + self.deadline = 0 + self._timeout = AsyncTimeout(DEFAULT_TIMEOUT) + + def _check_timeout(self): + if time.time() > self.deadline: + raise TimeoutException('connection timed out') + + def create_connection(self, address, timeout=None, source_address=None): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + timeout = AsyncTimeout.fromTimeout(timeout) + self._timeout = timeout + + host, port = address + err = None + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + sock.setblocking(False) # this is obviously critical + self.deadline = time.time() + timeout.getConnectTimeout() + # sock.settimeout(timeout) + + if source_address: + sock.bind(source_address) + for msg in self._connect(sock, sa): + if self._canceled or ABORT_FLAG_FUNCTION(): + raise CanceledException('Request canceled') + sock.setblocking(True) + return sock + + except socket.error as _: + err = _ + if sock is not None: + sock.shutdown(socket.SHUT_RDWR) + sock.close() + + if err is not None: + raise err + else: + raise socket.error("getaddrinfo returns an empty list") + + def _connect(self, sock, sa): + while not self._canceled and not ABORT_FLAG_FUNCTION(): + time.sleep(0.01) + self._check_timeout() # this should be done at the beginning of each loop + status = sock.connect_ex(sa) + if not status or status in (errno.EISCONN, WIN_EISCONN): + break + elif status in (errno.EINPROGRESS, WIN_EWOULDBLOCK): + self.deadline = time.time() + self._timeout.getConnectTimeout() + # elif status in (errno.EWOULDBLOCK, errno.EALREADY) or (os.name == 'nt' and status == errno.WSAEINVAL): + # pass + yield + + if self._canceled or ABORT_FLAG_FUNCTION(): + raise CanceledException('Request canceled') + + error = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if error: + # TODO: determine when this case can actually happen + raise socket.error((error,)) + + def _new_conn(self): + sock = self.create_connection( + address=(self.host, self.port), + timeout=self.timeout + ) + + return sock + + def cancel(self): + self._canceled = True + + +class AsyncHTTPConnection(HTTPConnection): + def __init__(self, *args, **kwargs): + HTTPConnection.__init__(self, *args, **kwargs) + self._canceled = False + self.deadline = 0 + + def cancel(self): + self._canceled = True + + +class AsyncHTTPConnectionPool(HTTPConnectionPool): + def __init__(self, *args, **kwargs): + HTTPConnectionPool.__init__(self, *args, **kwargs) + self.connections = [] + + def _new_conn(self): + """ + Return a fresh :class:`httplib.HTTPConnection`. + """ + self.num_connections += 1 + + extra_params = {} + if six.PY2: + extra_params['strict'] = self.strict + + conn = AsyncHTTPConnection(host=self.host, port=self.port, timeout=self.timeout.connect_timeout, **extra_params) + + # Backport fix LP #1412545 + if getattr(conn, '_tunnel_host', None): + # TODO: Fix tunnel so it doesn't depend on self.sock state. + conn._tunnel() + # Mark this connection as not reusable + conn.auto_open = 0 + + self.connections.append(conn) + + return conn + + def cancel(self): + for c in self.connections: + c.cancel() + + +class AsyncHTTPSConnectionPool(HTTPSConnectionPool): + def __init__(self, *args, **kwargs): + HTTPSConnectionPool.__init__(self, *args, **kwargs) + self.connections = [] + + def _new_conn(self): + """ + Return a fresh :class:`httplib.HTTPSConnection`. + """ + self.num_connections += 1 + + actual_host = self.host + actual_port = self.port + if self.proxy is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + connection_class = AsyncVerifiedHTTPSConnection + + extra_params = {} + if six.PY2: + extra_params['strict'] = self.strict + connection = connection_class(host=actual_host, port=actual_port, timeout=self.timeout.connect_timeout, **extra_params) + + self.connections.append(connection) + + try: + return self._prepare_conn(connection) + except AttributeError: + # urllib3 2.1.0 + return connection + + def cancel(self): + for c in self.connections: + c.cancel() + + +pool_classes_by_scheme = { + 'http': AsyncHTTPConnectionPool, + 'https': AsyncHTTPSConnectionPool, +} + + +class AsyncPoolManager(PoolManager): + def _new_pool(self, scheme, host, port, request_context=None): + """ + Create a new :class:`ConnectionPool` based on host, port and scheme. + + This method is used to actually create the connection pools handed out + by :meth:`connection_from_url` and companion methods. It is intended + to be overridden for customization. + """ + pool_cls = pool_classes_by_scheme[scheme] + kwargs = self.connection_pool_kw + if scheme == 'http': + kwargs = self.connection_pool_kw.copy() + for kw in SSL_KEYWORDS: + kwargs.pop(kw, None) + + return pool_cls(host, port, **kwargs) + + +class AsyncHTTPAdapter(HTTPAdapter): + def cancel(self): + for c in self.connections: + c.cancel() + + def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK): + """Initializes a urllib3 PoolManager. This method should not be called + from user code, and is only exposed for use when subclassing the + :class:`HTTPAdapter `. + + :param connections: The number of urllib3 connection pools to cache. + :param maxsize: The maximum number of connections to save in the pool. + :param block: Block when no free connections are available. + """ + # save these values for pickling + self._pool_connections = connections + self._pool_maxsize = maxsize + self._pool_block = block + + self.poolmanager = AsyncPoolManager(num_pools=connections, maxsize=maxsize, block=block) + self.connections = [] + + def get_connection(self, url, proxies=None): + """Returns a urllib3 connection for the given URL. This should not be + called from user code, and is only exposed for use when subclassing the + :class:`HTTPAdapter `. + + :param url: The URL to connect to. + :param proxies: (optional) A Requests-style dictionary of proxies used on this request. + """ + proxies = proxies or {} + proxy = proxies.get(urlparse(url.lower()).scheme) + + if proxy: + proxy_headers = self.proxy_headers(proxy) + + if proxy not in self.proxy_manager: + self.proxy_manager[proxy] = proxy_from_url( + proxy, + proxy_headers=proxy_headers, + num_pools=self._pool_connections, + maxsize=self._pool_maxsize, + block=self._pool_block + ) + + conn = self.proxy_manager[proxy].connection_from_url(url) + else: + # Only scheme should be lower case + parsed = urlparse(url) + url = parsed.geturl() + conn = self.poolmanager.connection_from_url(url) + + self.connections.append(conn) + return conn + + +class Session(requests.Session): + def __init__(self, *args, **kwargs): + requests.Session.__init__(self, *args, **kwargs) + self.mount('https://', AsyncHTTPAdapter()) + self.mount('http://', AsyncHTTPAdapter()) + + def cancel(self): + for v in self.adapters.values(): + v.close() + v.cancel() diff --git a/script.plexmod/lib/_included_packages/plexnet/audio.py b/script.plexmod/lib/_included_packages/plexnet/audio.py new file mode 100644 index 000000000..251c19b5e --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/audio.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from . import plexobjects +from . import plexmedia +from . import media + + +class Audio(media.MediaItem): + def __init__(self, *args, **kwargs): + self._settings = None + media.MediaItem.__init__(self, *args, **kwargs) + + def __eq__(self, other): + return self.ratingKey == other.ratingKey + + def __ne__(self, other): + return not self.__eq__(other) + + def _setData(self, data): + for k, v in data.attrib.items(): + setattr(self, k, plexobjects.PlexValue(v, self)) + + self.key = plexobjects.PlexValue(self.key.replace('/children', ''), self) + + def isMusicItem(self): + return True + + +@plexobjects.registerLibType +class Artist(Audio): + TYPE = 'artist' + + def _setData(self, data): + Audio._setData(self, data) + if self.isFullObject(): + self.countries = plexobjects.PlexItemList(data, media.Country, media.Country.TYPE, server=self.server) + self.genres = plexobjects.PlexItemList(data, media.Genre, media.Genre.TYPE, server=self.server) + self.similar = plexobjects.PlexItemList(data, media.Similar, media.Similar.TYPE, server=self.server) + + def albums(self): + path = '%s/children' % self.key + return plexobjects.listItems(self.server, path, Album.TYPE) + + def album(self, title): + path = '%s/children' % self.key + return plexobjects.findItem(self.server, path, title) + + def tracks(self, watched=None): + leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey + return plexobjects.listItems(self.server, leavesKey, watched=watched) + + def all(self): + return self.tracks() + + def track(self, title): + path = '/library/metadata/%s/allLeaves' % self.ratingKey + return plexobjects.findItem(self.server, path, title) + + def isFullObject(self): + # plex bug? http://bit.ly/1Sc2J3V + fixed_key = self.key.replace('/children', '') + return self.initpath == fixed_key + + def refresh(self): + self.server.query('/library/metadata/%s/refresh' % self.ratingKey) + + +@plexobjects.registerLibType +class Album(Audio): + TYPE = 'album' + + def _setData(self, data): + Audio._setData(self, data) + if self.isFullObject(): + self.genres = plexobjects.PlexItemList(data, media.Genre, media.Genre.TYPE, server=self.server) + + @property + def defaultTitle(self): + return self.parentTitle or self.title + + def tracks(self, watched=None): + path = '%s/children' % self.key + return plexobjects.listItems(self.server, path, watched=watched) + + def track(self, title): + path = '%s/children' % self.key + return plexobjects.findItem(self.server, path, title) + + def all(self): + return self.tracks() + + def isFullObject(self): + # plex bug? http://bit.ly/1Sc2J3V + fixed_key = self.key.replace('/children', '') + return self.initpath == fixed_key + + def artist(self): + return plexobjects.listItems(self.server, self.parentKey)[0] + + def watched(self): + return self.tracks(watched=True) + + def unwatched(self): + return self.tracks(watched=False) + + +@plexobjects.registerLibType +class Track(Audio): + TYPE = 'track' + + def _setData(self, data): + Audio._setData(self, data) + if self.isFullObject(): + self.moods = plexobjects.PlexItemList(data, media.Mood, media.Mood.TYPE, server=self.server) + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self) + + # data for active sessions + self.user = self._findUser(data) + self.player = self._findPlayer(data) + self.transcodeSession = self._findTranscodeSession(data) + + @property + def defaultTitle(self): + return self.parentTitle or self.title + + @property + def settings(self): + if not self._settings: + from . import plexapp + self._settings = plexapp.PlayerSettingsInterface() + + return self._settings + + @property + def thumbUrl(self): + return self.server.url(self.parentThumb) + + def album(self): + return plexobjects.listItems(self.server, self.parentKey)[0] + + def artist(self): + return plexobjects.listItems(self.server, self.grandparentKey)[0] + + def getStreamURL(self, **params): + return self._getStreamURL(**params) + + @property + def defaultThumb(self): + return self.__dict__.get('thumb') or self.__dict__.get('parentThumb') or self.get('grandparentThumb') + + @property + def defaultArt(self): + return self.__dict__.get('art') or self.get('grandparentArt') diff --git a/script.plexmod/lib/_included_packages/plexnet/audioobject.py b/script.plexmod/lib/_included_packages/plexnet/audioobject.py new file mode 100644 index 000000000..453e5e604 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/audioobject.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import +from . import http +from . import mediadecisionengine +from . import util + + +class AudioObjectClass(object): + def __init__(self, item): + self.containerFormats = { + 'aac': "es.aac-adts" + } + + self.item = item + self.choice = mediadecisionengine.MediaDecisionEngine().chooseMedia(item) + if self.choice: + self.media = self.choice.media + self.lyrics = None # createLyrics(item, self.media) + + def build(self, directPlay=None): + directPlay = directPlay or self.choice.isDirectPlayable + + obj = util.AttributeDict() + + # TODO(schuyler): Do we want/need to add anything generic here? Title? Duration? + + if directPlay: + obj = self.buildDirectPlay(obj) + else: + obj = self.buildTranscode(obj) + + self.metadata = obj + + util.LOG("Constructed audio item for playback: {0}".format(obj)) + + return self.metadata + + def buildTranscode(self, obj): + transcodeServer = self.item.getTranscodeServer(True, "audio") + if not transcodeServer: + return None + + obj.streamFormat = "mp3" + obj.isTranscoded = True + obj.transcodeServer = transcodeServer + + builder = http.HttpRequest(transcodeServer.buildUrl("/music/:/transcode/universal/start.m3u8", True)) + builder.addParam("protocol", "http") + builder.addParam("path", self.item.getAbsolutePath("key")) + builder.addParam("session", self.item.getGlobal("clientIdentifier")) + builder.addParam("directPlay", "0") + builder.addParam("directStream", "0") + + obj.url = builder.getUrl() + + return obj + + def buildDirectPlay(self, obj): + if self.choice.part: + obj.url = self.item.getServer().buildUrl(self.choice.part.getAbsolutePath("key"), True) + + # Set and override the stream format if applicable + obj.streamFormat = self.choice.media.container or 'mp3' + if self.containerFormats.get(obj.streamFormat): + obj.streamFormat = self.containerFormats[obj.streamFormat] + + # If we're direct playing a FLAC, bitrate can be required, and supposedly + # this is the only way to do it. plexinc/roku-client#48 + # + bitrate = self.choice.media.getInt("bitrate") + if bitrate > 0: + obj.streams = [{'url': obj.url, 'bitrate': bitrate}] + + return obj + + # We may as well fallback to transcoding if we could not direct play + return self.buildTranscode(obj) + + def getLyrics(self): + return self.lyrics + + def hasLyrics(self): + return False + # return self.lyrics.isAvailable() diff --git a/script.plexmod/lib/_included_packages/plexnet/callback.py b/script.plexmod/lib/_included_packages/plexnet/callback.py new file mode 100644 index 000000000..d99a86f50 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/callback.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import +import threading + + +class Callable(object): + _currID = 0 + + def __init__(self, func, forcedArgs=None, ID=None): + self.func = func + self.forcedArgs = forcedArgs + + self.ID = ID or id(func) + + if not self.ID: + self.ID = Callable.nextID() + + def __repr__(self): + return ''.format(repr(self.func).strip('<>')) + + def __eq__(self, other): + if not other: + return False + + if self.__class__ != other.__class__: + return False + + return self.ID and self.ID == other.ID + + def __ne__(self, other): + return not self.__eq__(other) + + def __call__(self, *args, **kwargs): + args = args or [] + if self.forcedArgs: + args = self.forcedArgs + + self.func(*args, **kwargs) + + @property + def context(self): + return self.func.__self__ + + @classmethod + def nextID(cls): + cls._currID += 1 + return cls._currID + + def deferCall(self, timeout=0.1): + timer = threading.Timer(timeout, self.onDeferCallTimer) + timer.name = 'ONDEFERCALLBACK-TIMER:{0}'.format(self.func) + timer.start() + + def onDeferCallTimer(self): + self() diff --git a/script.plexmod/lib/_included_packages/plexnet/captions.py b/script.plexmod/lib/_included_packages/plexnet/captions.py new file mode 100644 index 000000000..e3b49fe21 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/captions.py @@ -0,0 +1,80 @@ +from __future__ import absolute_import +from . import plexapp +from . import util + + +class Captions(object): + def __init__(self): + self.deviceInfo = util.INTERFACE.getGlobal("deviceInfo") + + self.textSize = util.AttributeDict({ + 'extrasmall': 15, + 'small': 20, + 'medium': 30, + 'large': 45, + 'extralarge': 65, + }) + + self.burnedSize = util.AttributeDict({ + 'extrasmall': "60", + 'small': "80", + 'medium': "100", + 'large': "135", + 'extralarge': "200" + }) + + self.colors = util.AttributeDict({ + 'white': 0xffffffff, + 'black': 0x000000ff, + 'red': 0xff0000ff, + 'green': 0x008000ff, + 'blue': 0x0000ffff, + 'yellow': 0xffff00ff, + 'magenta': 0xff00ffff, + 'cyan': 0x00ffffff, + }) + + self.defaults = util.AttributeDict({ + 'textSize': self.textSize.medium, + 'textColor': self.colors.white, + 'textOpacity': 80, + 'backgroundColor': self.colors.black, + 'backgroundOpacity': 70, + 'burnedSize': None + }) + + def getTextSize(self): + value = self.getOption("Text/Size") + return self.textSize.get(value) or self.defaults.textSize + + def getTextColor(self): + value = self.getOption("Text/Color") + return self.colors.get(value) or self.defaults.textColor + + def getTextOpacity(self): + value = self.getOption("Text/Opacity") + if value is None or value == "default": + return self.defaults.textOpacity + else: + return int(value) + + def getBackgroundColor(self): + value = self.getOption("Background/Color") + return self.colors.get(value) or self.defaults.backgroundColor + + def getBackgroundOpacity(self): + value = self.getOption("Background/Opacity") + if value is None or value == "default": + return self.defaults.backgroundOpacity + else: + return int(value) + + def getBurnedSize(self): + value = self.getOption("Text/Size") + return self.burnedSize.get(value) or self.defaults.burnedSize + + def getOption(self, key): + opt = self.deviceInfo.getCaptionsOption(key) + return opt is not None and opt.lower().replace(' ', '') or None + +CAPTIONS = Captions() diff --git a/script.plexmod/lib/_included_packages/plexnet/compat.py b/script.plexmod/lib/_included_packages/plexnet/compat.py new file mode 100644 index 000000000..96b0d7fce --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/compat.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Python 2/3 compatability +Always try Py3 first +""" + +from __future__ import absolute_import +try: + from urllib.parse import urlencode +except ImportError: + from six.moves.urllib.parse import urlencode + +try: + from urllib.parse import quote +except ImportError: + from six.moves.urllib.parse import quote + +try: + from urllib.parse import quote_plus +except ImportError: + from six.moves.urllib.parse import quote_plus + +try: + from configparser import ConfigParser +except ImportError: + from six.moves.configparser import ConfigParser diff --git a/script.plexmod/lib/_included_packages/plexnet/exceptions.py b/script.plexmod/lib/_included_packages/plexnet/exceptions.py new file mode 100644 index 000000000..a3adf39e8 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/exceptions.py @@ -0,0 +1,22 @@ +class BadRequest(Exception): + pass + + +class NotFound(Exception): + pass + + +class UnknownType(Exception): + pass + + +class Unsupported(Exception): + pass + + +class Unauthorized(Exception): + pass + + +class ServerNotOwned(Exception): + pass diff --git a/script.plexmod/lib/_included_packages/plexnet/gdm.py b/script.plexmod/lib/_included_packages/plexnet/gdm.py new file mode 100644 index 000000000..90b9eba7a --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/gdm.py @@ -0,0 +1,347 @@ +from __future__ import absolute_import +import threading +import socket +import traceback +import time +from . import util +from . import netif + +from . import plexconnection + +DISCOVERY_PORT = 32414 +WIN_NL = chr(13) + chr(10) + + +class GDMDiscovery(object): + def __init__(self): + self._close = False + self.thread = None + + # def isActive(self): + # util.LOG('GDMDiscovery().isActive() - NOT IMPLEMENTED') + # return False + + # def discover(self): + # util.LOG('GDMDiscovery().discover() - NOT IMPLEMENTED') + + def isActive(self): + from . import plexapp + return util.INTERFACE.getPreference("gdm_discovery", True) and self.thread and self.thread.is_alive() + + ''' + def discover(self): + # Only allow discovery if enabled and not currently running + self._close = False + import plexapp + if not util.INTERFACE.getPreference("gdm_discovery", True) or self.isActive(): + return + + ifaces = netif.getInterfaces() + + message = "M-SEARCH * HTTP/1.1" + WIN_NL + WIN_NL + + # Broadcasting to 255.255.255.255 only works on some Rokus, but we + # can't reliably determine the broadcast address for our current + # interface. Try assuming a /24 network, and then fall back to the + # multicast address if that doesn't work. + + multicast = "239.0.0.250" + ip = multicast + subnetRegex = re.compile("((\d+)\.(\d+)\.(\d+)\.)(\d+)") + addr = getFirstIPAddress() # TODO:: -------------------------------------------------------------------------------------------------------- HANDLE + if addr: + match = subnetRegex.search(addr) + if match: + ip = match.group(1) + "255" + util.DEBUG_LOG("Using broadcast address {0}".format()) + + # Socket things sometimes fail for no good reason, so try a few times. + attempt = 0 + success = False + + while attempt < 5 and not success: + udp = CreateObject("roDatagramSocket") + udp.setMessagePort(Application().port) + udp.setBroadcast(true) + + # More things that have been observed to be flaky. + for i in range(5): + addr = CreateObject("roSocketAddress") + addr.setHostName(ip) + addr.setPort(32414) + udp.setSendToAddress(addr) + + sendTo = udp.getSendToAddress() + if sendTo: + sendToStr = str(sendTo.getAddress()) + addrStr = str(addr.getAddress()) + util.DEBUG_LOG("GDM sendto address: " + sendToStr + " / " + addrStr) + if sendToStr == addrStr: + break + + util.ERROR_LOG("Failed to set GDM sendto address") + + udp.notifyReadable(true) + bytesSent = udp.sendStr(message) + util.DEBUG_LOG("Sent " + str(bytesSent) + " bytes") + if bytesSent > 0: + success = udp.eOK() + else: + success = False + if bytesSent == 0 and ip != multicast: + util.LOG("Falling back to multicast address") + ip = multicast + attempt = 0 + + if success: + break + elif attempt == 4 and ip != multicast: + util.LOG("Falling back to multicast address") + ip = multicast + attempt = 0 + else: + time.sleep(500) + util.WARN_LOG("Retrying GDM, errno=" + str(udp.status())) + attempt += 1 + + if success: + util.DEBUG_LOG("Successfully sent GDM discovery message, waiting for servers") + self.servers = [] + self.timer = plexapp.createTimer(5000, self.onTimer) + self.socket = udp + Application().AddSocketCallback(udp, createCallable("OnSocketEvent", m)) + util.APP.addTimer(self.timer) + else: + util.ERROR_LOG("Failed to send GDM discovery message") + import plexapp + import plexresource + plexapp.SERVERMANAGER.UpdateFromConnectionType([], plexresource.ResourceConnection.SOURCE_DISCOVERED) + self.socket = None + self.timer = None + ''' + + def discover(self): + from . import plexapp + if not util.INTERFACE.getPreference("gdm_discovery", True) or self.isActive(): + return + + self.thread = threading.Thread(target=self._discover) + self.thread.start() + + def _discover(self): + ifaces = netif.getInterfaces() + sockets = [] + self.servers = [] + + packet = ("M-SEARCH * HTTP/1.1" + WIN_NL + WIN_NL).encode("utf-8") + + for i in ifaces: + if not i.broadcast: + continue + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0.01) # 10ms + s.bind((i.ip, 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sockets.append((s, i)) + + success = False + + for attempt in (0, 1): + for s, i in sockets: + if self._close: + return + util.DEBUG_LOG(' o-> Broadcasting to {0}: {1}'.format(i.name, i.broadcast)) + try: + s.sendto(packet, (i.broadcast, DISCOVERY_PORT)) + success = True + except: + util.ERROR() + + if success: + break + + end = time.time() + 5 + + while time.time() < end: + for s, i in sockets: + if self._close: + return + try: + message, address = s.recvfrom(4096) + self.onSocketEvent(message, address) + except socket.timeout: + pass + except: + traceback.print_exc() + + self.discoveryFinished() + + def onSocketEvent(self, message, addr): + util.DEBUG_LOG('Received GDM message:\n' + str(message)) + + hostname = addr[0] # socket.gethostbyaddr(addr[0])[0] + + name = parseFieldValue(message, b"Name: ") + port = parseFieldValue(message, b"Port: ") or b"32400" + machineID = parseFieldValue(message, b"Resource-Identifier: ") + secureHost = parseFieldValue(message, b"Host: ") + + util.DEBUG_LOG("Received GDM response for " + repr(name) + " at http://" + hostname + ":" + port) + + if not name or not machineID: + return + + from . import plexserver + conn = plexconnection.PlexConnection(plexconnection.PlexConnection.SOURCE_DISCOVERED, "http://" + hostname + ":" + port, True, None, bool(secureHost)) + server = plexserver.createPlexServerForConnection(conn) + server.uuid = machineID + server.name = name + server.sameNetwork = True + + # If the server advertised a secure hostname, add a secure connection as well, and + # set the http connection as a fallback. + # + if secureHost: + server.connections.insert( + 0, + plexconnection.PlexConnection( + plexconnection.PlexConnection.SOURCE_DISCOVERED, "https://" + hostname.replace(".", "-") + "." + secureHost + ":" + port, True, None + ) + ) + + self.servers.append(server) + + def discoveryFinished(self, *args, **kwargs): + # Time's up, report whatever we found + self.close() + + if self.servers: + util.LOG("Finished GDM discovery, found {0} server(s)".format(len(self.servers))) + from . import plexapp + plexapp.SERVERMANAGER.updateFromConnectionType(self.servers, plexconnection.PlexConnection.SOURCE_DISCOVERED) + self.servers = None + + def close(self): + self._close = True + + +def parseFieldValue(message, label): + if label not in message: + return None + + return message.split(label, 1)[-1].split(chr(13).encode())[0].decode() + + +DISCOVERY = GDMDiscovery() + +''' +# GDM Advertising + +class GDMAdvertiser(object): + + def __init__(self): + self.responseString = None + + def createSocket() + listenAddr = CreateObject("roSocketAddress") + listenAddr.setPort(32412) + listenAddr.setAddress("0.0.0.0") + + udp = CreateObject("roDatagramSocket") + + if not udp.setAddress(listenAddr) then + Error("Failed to set address on GDM advertiser socket") + return + end if + + if not udp.setBroadcast(true) then + Error("Failed to set broadcast on GDM advertiser socket") + return + end if + + udp.notifyReadable(true) + udp.setMessagePort(Application().port) + + m.socket = udp + + Application().AddSocketCallback(udp, createCallable("OnSocketEvent", m)) + + Debug("Created GDM player advertiser") + + + def refresh() + # Always regenerate our response, even if it might not have changed, it's + # just not that expensive. + m.responseString = invalid + + enabled = AppSettings().GetBoolPreference("remotecontrol") + if enabled AND m.socket = invalid then + m.CreateSocket() + else if not enabled AND m.socket <> invalid then + m.Close() + end if + + + def cleanup() + m.Close() + fn = function() :m.GDMAdvertiser = invalid : + fn() + + + def onSocketEvent(msg as object) + # PMS polls every five seconds, so this is chatty when not debugging. + # Debug("Got a GDM advertiser socket event, is readable: " + tostr(m.socket.isReadable())) + + if m.socket.isReadable() then + message = m.socket.receiveStr(4096) + endIndex = instr(1, message, chr(13)) - 1 + if endIndex <= 0 then endIndex = message.Len() + line = Mid(message, 1, endIndex) + + if line = "M-SEARCH * HTTP/1.1" then + response = m.GetResponseString() + + # Respond directly to whoever sent the search message. + sock = CreateObject("roDatagramSocket") + sock.setSendToAddress(m.socket.getReceivedFromAddress()) + bytesSent = sock.sendStr(response) + sock.Close() + if bytesSent <> Len(response) then + Error("GDM player response only sent " + tostr(bytesSent) + " bytes out of " + tostr(Len(response))) + end if + else + Error("Received unexpected message on GDM advertiser socket: " + tostr(line) + ";") + end if + end if + + + def getResponseString() as string + if m.responseString = invalid then + buf = box("HTTP/1.0 200 OK" + WinNL()) + + settings = AppSettings() + + appendNameValue(buf, "Name", settings.GetGlobal("friendlyName")) + appendNameValue(buf, "Port", WebServer().port.tostr()) + appendNameValue(buf, "Product", "Plex for Roku") + appendNameValue(buf, "Content-Type", "plex/media-player") + appendNameValue(buf, "Protocol", "plex") + appendNameValue(buf, "Protocol-Version", "1") + appendNameValue(buf, "Protocol-Capabilities", "timeline,playback,navigation,playqueues") + appendNameValue(buf, "Version", settings.GetGlobal("appVersionStr")) + appendNameValue(buf, "Resource-Identifier", settings.GetGlobal("clientIdentifier")) + appendNameValue(buf, "Device-Class", "stb") + + m.responseString = buf + + Debug("Built GDM player response:" + m.responseString) + end if + + return m.responseString + + + sub appendNameValue(buf, name, value) + line = name + ": " + value + WinNL() + buf.AppendString(line, Len(line)) + +''' diff --git a/script.plexmod/lib/_included_packages/plexnet/http.py b/script.plexmod/lib/_included_packages/plexnet/http.py new file mode 100644 index 000000000..d3df0b98d --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/http.py @@ -0,0 +1,329 @@ +from __future__ import absolute_import +import sys +import os +import re +import traceback +import requests +import socket +import urllib3 +from . import threadutils +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import mimetypes +from . import plexobjects +from xml.etree import ElementTree + +from . import asyncadapter + +from . import callback +from . import util + + +codes = requests.codes +status_codes = requests.status_codes._codes + + +DEFAULT_TIMEOUT = asyncadapter.AsyncTimeout(util.TIMEOUT).setConnectTimeout(util.TIMEOUT) + + +def GET(*args, **kwargs): + return requests.get(*args, headers=util.BASE_HEADERS.copy(), timeout=util.TIMEOUT, **kwargs) + + +def POST(*args, **kwargs): + return requests.post(*args, headers=util.BASE_HEADERS.copy(), timeout=util.TIMEOUT, **kwargs) + + +def Session(): + s = asyncadapter.Session() + s.headers = util.BASE_HEADERS.copy() + s.timeout = util.TIMEOUT + + return s + + +class RequestContext(dict): + def __getattr__(self, attr): + return self.get(attr) + + def __setattr__(self, attr, value): + self[attr] = value + + +class HttpRequest(object): + _cancel = False + + def __init__(self, url, method=None, forceCertificate=False): + self.server = None + self.path = None + self.hasParams = '?' in url + self.ignoreResponse = False + self.session = asyncadapter.Session() + self.session.headers = util.BASE_HEADERS.copy() + self.currentResponse = None + self.method = method + self.url = url + self.thread = None + + # Use our specific plex.direct CA cert if applicable to improve performance + # if forceCertificate or url[:5] == "https": # TODO: ---------------------------------------------------------------------------------IMPLEMENT + # certsPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'certs') + # if "plex.direct" in url: + # self.session.cert = os.path.join(certsPath, 'plex-bundle.crt') + # else: + # self.session.cert = os.path.join(certsPath, 'ca-bundle.crt') + + def removeAsPending(self): + from . import plexapp + util.APP.delRequest(self) + + def startAsync(self, *args, **kwargs): + self.thread = threadutils.KillableThread(target=self._startAsync, args=args, kwargs=kwargs, name='HTTP-ASYNC:{0}'.format(self.url)) + self.thread.start() + return True + + def _startAsync(self, body=None, contentType=None, context=None): + timeout = context and context.timeout or DEFAULT_TIMEOUT + self.logRequest(body, timeout) + if self._cancel: + return + try: + if self.method == 'PUT': + res = self.session.put(self.url, timeout=timeout, stream=True) + elif self.method == 'DELETE': + res = self.session.delete(self.url, timeout=timeout, stream=True) + elif self.method == 'HEAD': + res = self.session.head(self.url, timeout=timeout, stream=True) + elif self.method == 'POST' or body is not None: + if not contentType: + self.session.headers["Content-Type"] = "application/x-www-form-urlencoded" + else: + self.session.headers["Content-Type"] = mimetypes.guess_type(contentType) + + res = self.session.post(self.url, data=body or None, timeout=timeout, stream=True) + else: + res = self.session.get(self.url, timeout=timeout, stream=True) + self.currentResponse = res + + if self._cancel: + return + except asyncadapter.TimeoutException: + from . import plexapp + plexapp.util.APP.onRequestTimeout(context) + self.removeAsPending() + return + except asyncadapter.CanceledException: + return + except (urllib3.exceptions.ProtocolError, requests.exceptions.ConnectionError): + self.removeAsPending() + return + except Exception as e: + util.ERROR('Request failed {0}'.format(util.cleanToken(self.url))) + if not hasattr(e, 'response'): + return + res = e.response + + self.onResponse(res, context) + + self.removeAsPending() + + def getWithTimeout(self, seconds=DEFAULT_TIMEOUT): + return HttpObjectResponse(self.getPostWithTimeout(seconds), self.path, self.server) + + def postWithTimeout(self, seconds=DEFAULT_TIMEOUT, body=None): + self.method = 'POST' + return HttpObjectResponse(self.getPostWithTimeout(seconds, body), self.path, self.server) + + def getToStringWithTimeout(self, seconds=DEFAULT_TIMEOUT): + res = self.getPostWithTimeout(seconds) + if not res: + return '' + return res.text.encode('utf8') + + def postToStringWithTimeout(self, body=None, seconds=DEFAULT_TIMEOUT): + self.method = 'POST' + res = self.getPostWithTimeout(seconds, body) + if not res: + return '' + return res.text.encode('utf8') + + def getPostWithTimeout(self, seconds=DEFAULT_TIMEOUT, body=None): + if self._cancel: + return + + self.logRequest(body, seconds, False) + try: + if self.method == 'PUT': + res = self.session.put(self.url, timeout=seconds, stream=True) + elif self.method == 'DELETE': + res = self.session.delete(self.url, timeout=seconds, stream=True) + elif self.method == 'HEAD': + res = self.session.head(self.url, timeout=seconds, stream=True) + elif self.method == 'POST' or body is not None: + res = self.session.post(self.url, data=body, timeout=seconds, stream=True) + else: + res = self.session.get(self.url, timeout=seconds, stream=True) + + self.currentResponse = res + + if self._cancel: + return None + + util.LOG("Got a {0} from {1}".format(res.status_code, util.cleanToken(self.url))) + # self.event = msg + return res + except Exception as e: + info = traceback.extract_tb(sys.exc_info()[2])[-1] + util.WARN_LOG( + "Request errored out - URL: {0} File: {1} Line: {2} Msg: {3}".format(util.cleanToken(self.url), os.path.basename(info[0]), info[1], getattr(e, 'message', '')) + ) + + return None + + def wasOK(self): + return self.currentResponse and self.currentResponse.ok + + def wasNotFound(self): + return self.currentResponse is not None and self.currentResponse.status_code == requests.codes.not_found + + def getIdentity(self): + return str(id(self)) + + def getUrl(self): + return self.url + + def getRelativeUrl(self): + url = self.getUrl() + m = re.match(r'^\w+://.+?(/.+)', url) + if m: + return m.group(1) + return url + + def killSocket(self): + if not self.currentResponse: + return + + try: + socket.fromfd(self.currentResponse.raw.fileno(), socket.AF_INET, socket.SOCK_STREAM).shutdown(socket.SHUT_RDWR) + return + except AttributeError: + pass + except Exception as e: + util.ERROR(err=e) + + try: + self.currentResponse.raw._fp.fp._sock.shutdown(socket.SHUT_RDWR) + except AttributeError: + pass + except Exception as e: + util.ERROR(err=e) + + def cancel(self): + self._cancel = True + self.session.cancel() + self.removeAsPending() + self.killSocket() + + def addParam(self, encodedName, value): + if self.hasParams: + self.url += "&" + encodedName + "=" + six.moves.urllib.parse.quote_plus(value) + else: + self.hasParams = True + self.url += "?" + encodedName + "=" + six.moves.urllib.parse.quote_plus(value) + + def addHeader(self, name, value): + self.session.headers[name] = value + + def createRequestContext(self, requestType, callback_=None, timeout=None): + context = RequestContext() + context.requestType = requestType + context.timeout = timeout or DEFAULT_TIMEOUT + + if callback_: + context.callback = callback.Callable(self.onResponse) + context.completionCallback = callback_ + context.callbackCtx = callback_.context + + return context + + def onResponse(self, event, context): + if context.completionCallback: + response = HttpResponse(event) + context.completionCallback(self, response, context) + + def logRequest(self, body, timeout=None, _async=True): + # Log the real request method + method = self.method + if not method: + method = body is not None and "POST" or "GET" + util.LOG( + "Starting request: {0} {1} (async={2} timeout={3})".format(method, util.cleanToken(self.url), + _async, timeout) + ) + + +class HttpResponse(object): + def __init__(self, event): + self.event = event + if not self.event is None: + self.event.content # force data to be read + self.event.close() + + def isSuccess(self): + if not self.event: + return False + return self.event.status_code >= 200 and self.event.status_code < 300 + + def isError(self): + return not self.isSuccess() + + def getStatus(self): + if self.event is None: + return 0 + return self.event.status_code + + def getBodyString(self): + if self.event is None: + return '' + return self.event.text.encode('utf-8') + + def getErrorString(self): + if self.event is None: + return '' + return self.event.reason + + def getBodyXml(self): + if not self.event is None: + return ElementTree.fromstring(self.getBodyString()) + + return None + + def getResponseHeader(self, name): + if self.event is None: + return None + return self.event.headers.get(name) + + +class HttpObjectResponse(HttpResponse, plexobjects.PlexContainer): + def __init__(self, response, path, server=None): + self.event = response + if self.event: + self.event.content # force data to be read + self.event.close() + + data = self.getBodyXml() + + plexobjects.PlexContainer.__init__(self, data, initpath=path, server=server, address=path) + self.container = self + + self.items = plexobjects.listItems(server, path, data=data, container=self) + + +def addRequestHeaders(transferObj, headers=None): + if isinstance(headers, dict): + for header in headers: + transferObj.addHeader(header, headers[header]) + util.DEBUG_LOG("Adding header to {0}: {1}: {2}".format(transferObj, header, headers[header])) + + +def addUrlParam(url, param): + return url + ('?' in url and '&' or '?') + param diff --git a/script.plexmod/lib/_included_packages/plexnet/locks.py b/script.plexmod/lib/_included_packages/plexnet/locks.py new file mode 100644 index 000000000..5e309f4ee --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/locks.py @@ -0,0 +1,67 @@ +# Generic Locks. These are only virtual. You will need to check for the lock to +# ignore processing depending on the lockName. +# * Locks().Lock("lockName") : creates virtual lock +# * Locks().IsLocked("lockName") : returns true if locked +# * Locks().Unlock("lockName") : return true if existed & removed +from __future__ import absolute_import +from . import util + + +class Locks(object): + def __init__(self): + self.locks = {} + self.oneTimeLocks = {} + + def lock(self, name): + self.locks[name] = (self.locks.get(name) or 0) + 1 + util.DEBUG_LOG("Lock {0}, total={0}".format(name, self.locks[name])) + + def lockOnce(self, name): + util.DEBUG_LOG("Locking once {0}".format(name)) + self.oneTimeLocks[name] = True + + def unlock(self, name, forceUnlock=False): + oneTime = False + if name in self.oneTimeLocks: + del self.oneTimeLocks[name] + oneTime = True + normal = (self.locks.get(name) or 0) > 0 + + if normal: + if forceUnlock: + self.locks[name] = 0 + else: + self.locks[name] -= 1 + + if self.locks[name] <= 0: + del self.locks[name] + else: + normal = False + + unlocked = (normal or oneTime) + util.DEBUG_LOG("Unlock {0}, total={1}, unlocked={2}".format(name, self.locks.get(name) or 0, unlocked)) + + return unlocked + + def isLocked(self, name): + return name in self.oneTimeLocks or name in self.locks + # return (self.oneTimeLocks.Delete(name) or self.locks.DoesExist(name)) + + +# lock helpers +def disableBackButton(): + LOCKS.lock("BackButton") + + +def enableBackButton(): + LOCKS.unlock("BackButton", True) + + +def disableRemoteControl(): + LOCKS.lock("roUniversalControlEvent") + + +def enableRemoteControl(): + LOCKS.unlock("roUniversalControlEvent", True) + +LOCKS = Locks() diff --git a/script.plexmod/lib/_included_packages/plexnet/media.py b/script.plexmod/lib/_included_packages/plexnet/media.py new file mode 100644 index 000000000..56d70eb02 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/media.py @@ -0,0 +1,258 @@ +from __future__ import absolute_import +from . import plexobjects +from . import plexstream +from . import util + +METADATA_RELATED_TRAILER = 1 +METADATA_RELATED_DELETED_SCENE = 2 +METADATA_RELATED_INTERVIEW = 3 +METADATA_RELATED_MUSIC_VIDEO = 4 +METADATA_RELATED_BEHIND_THE_SCENES = 5 +METADATA_RELATED_SCENE_OR_SAMPLE = 6 +METADATA_RELATED_LIVE_MUSIC_VIDEO = 7 +METADATA_RELATED_LYRIC_MUSIC_VIDEO = 8 +METADATA_RELATED_CONCERT = 9 +METADATA_RELATED_FEATURETTE = 10 +METADATA_RELATED_SHORT = 11 +METADATA_RELATED_OTHER = 12 + + +class MediaItem(plexobjects.PlexObject): + def getIdentifier(self): + identifier = self.get('identifier') or None + + if identifier is None: + identifier = self.container.identifier + + # HACK + # PMS doesn't return an identifier for playlist items. If we haven't found + # an identifier and the key looks like a library item, then we pretend like + # the identifier was set. + # + if identifier is None: # Modified from Roku code which had no check for None with iPhoto - is that right? + if self.key.startswith('/library/metadata'): + identifier = "com.plexapp.plugins.library" + elif self.isIPhoto(): + identifier = "com.plexapp.plugins.iphoto" + + return identifier + + def getQualityType(self, server=None): + if self.isOnlineItem(): + return util.QUALITY_ONLINE + + if not server: + server = self.getServer() + + return util.QUALITY_LOCAL if server.isLocalConnection() else util.QUALITY_REMOTE + + def delete(self): + if not self.ratingKey: + return + + from . import plexrequest + req = plexrequest.PlexRequest(self.server, '/library/metadata/{0}'.format(self.ratingKey), method='DELETE') + req.getToStringWithTimeout(10) + self.deleted = req.wasOK() + return self.deleted + + def exists(self): + if self.deleted or self.deletedAt: + return False + + data = self.server.query('/library/metadata/{0}'.format(self.ratingKey)) + return data is not None and data.attrib.get('size') != '0' + # req = plexrequest.PlexRequest(self.server, '/library/metadata/{0}'.format(self.ratingKey), method='HEAD') + # req.getToStringWithTimeout(10) + # return not req.wasNotFound() + + def fixedDuration(self): + duration = self.duration.asInt() + if duration < 1000: + duration *= 60000 + return duration + + +class Media(plexobjects.PlexObject): + TYPE = 'Media' + + def __init__(self, data, initpath=None, server=None, video=None): + plexobjects.PlexObject.__init__(self, data, initpath=initpath, server=server) + self.video = video + self.parts = [MediaPart(elem, initpath=self.initpath, server=self.server, media=self) for elem in data] + + def __repr__(self): + title = self.video.title.replace(' ', '.')[0:20] + return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8')) + + +class MediaPart(plexobjects.PlexObject): + TYPE = 'Part' + + def __init__(self, data, initpath=None, server=None, media=None): + plexobjects.PlexObject.__init__(self, data, initpath=initpath, server=server) + self.media = media + self.streams = [MediaPartStream.parse(e, initpath=self.initpath, server=server, part=self) for e in data if e.tag == 'Stream'] + + def __repr__(self): + return '<%s:%s>' % (self.__class__.__name__, self.id) + + def selectedStream(self, stream_type): + streams = [x for x in self.streams if stream_type == x.type] + selected = list([x for x in streams if x.selected is True]) + if len(selected) == 0: + return None + return selected[0] + + +class MediaPartStream(plexstream.PlexStream): + TYPE = None + STREAMTYPE = None + + def __init__(self, data, initpath=None, server=None, part=None): + plexobjects.PlexObject.__init__(self, data, initpath, server) + self.part = part + + @staticmethod + def parse(data, initpath=None, server=None, part=None): + STREAMCLS = { + 1: VideoStream, + 2: AudioStream, + 3: SubtitleStream + } + stype = int(data.attrib.get('streamType')) + cls = STREAMCLS.get(stype, MediaPartStream) + return cls(data, initpath=initpath, server=server, part=part) + + def __repr__(self): + return '<%s:%s>' % (self.__class__.__name__, self.id) + + +class VideoStream(MediaPartStream): + TYPE = 'videostream' + STREAMTYPE = plexstream.PlexStream.TYPE_VIDEO + + +class AudioStream(MediaPartStream): + TYPE = 'audiostream' + STREAMTYPE = plexstream.PlexStream.TYPE_AUDIO + + +class SubtitleStream(MediaPartStream): + TYPE = 'subtitlestream' + STREAMTYPE = plexstream.PlexStream.TYPE_SUBTITLE + + +class TranscodeSession(plexobjects.PlexObject): + TYPE = 'TranscodeSession' + + +class MediaTag(plexobjects.PlexObject): + TYPE = None + ID = 'None' + + def __repr__(self): + tag = self.tag.replace(' ', '.')[0:20] + return '<%s:%s:%s>' % (self.__class__.__name__, self.id, tag) + + def __eq__(self, other): + if other.__class__ != self.__class__: + return False + + return self.id == other.id + + def __ne__(self, other): + return not self.__eq__(other) + + +class Collection(MediaTag): + TYPE = 'Collection' + FILTER = 'collection' + + +class Country(MediaTag): + TYPE = 'Country' + FILTER = 'country' + + +class Director(MediaTag): + TYPE = 'Director' + FILTER = 'director' + ID = '4' + + +class Genre(MediaTag): + TYPE = 'Genre' + FILTER = 'genre' + ID = '1' + + +class Mood(MediaTag): + TYPE = 'Mood' + FILTER = 'mood' + + +class Producer(MediaTag): + TYPE = 'Producer' + FILTER = 'producer' + + +class Role(MediaTag): + TYPE = 'Role' + FILTER = 'actor' + ID = '6' + + def sectionRoles(self): + hubs = self.server.hubs(count=10, search_query=self.tag) + for hub in hubs: + if hub.type == 'actor': + break + else: + return None + + roles = [] + + for actor in hub.items: + if actor.id == self.id: + roles.append(actor) + + return roles or None + + +class Similar(MediaTag): + TYPE = 'Similar' + FILTER = 'similar' + + +class Writer(MediaTag): + TYPE = 'Writer' + FILTER = 'writer' + + +class Chapter(MediaTag): + TYPE = 'Chapter' + + def startTime(self): + return self.get('startTimeOffset', -1).asInt() + + +class Bandwidth(plexobjects.PlexObject): + TYPE = 'Bandwidth' + + +class Marker(MediaTag): + TYPE = 'Marker' + FILTER = 'Marker' + + +class Review(MediaTag): + TYPE = 'Review' + FILTER = 'Review' + + def ratingImage(self): + # only rottentomatoes currently supported + img = str(self.image) + if not img or not img.startswith("rottentomatoes://"): + return '' + + return img.split('rottentomatoes://')[1] diff --git a/script.plexmod/lib/_included_packages/plexnet/mediachoice.py b/script.plexmod/lib/_included_packages/plexnet/mediachoice.py new file mode 100644 index 000000000..a41f5c7c8 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/mediachoice.py @@ -0,0 +1,55 @@ +from __future__ import absolute_import +from . import plexstream +from . import util + + +class MediaChoice(object): + SUBTITLES_DEFAULT = 0 + SUBTITLES_BURN = 1 + SUBTITLES_SOFT_DP = 2 + SUBTITLES_SOFT_ANY = 3 + + def __init__(self, media=None, partIndex=0): + self.media = media + self.part = None + self.forceTranscode = False + self.isDirectPlayable = False + self.videoStream = None + self.audioStream = None + self.subtitleStream = None + self.isSelected = False + self.subtitleDecision = self.SUBTITLES_DEFAULT + + self.sorts = util.AttributeDict() + + if media: + self.indirectHeaders = media.indirectHeaders + self.part = media.parts[partIndex] + if not self.part: + for part in media.parts: + if part.isAccessible(): + self.part = part + + if self.part: + # We generally just rely on PMS to have told us selected streams, so + # initialize our streams accordingly. + + self.videoStream = self.part.getSelectedStreamOfType(plexstream.PlexStream.TYPE_VIDEO) + self.audioStream = self.part.getSelectedStreamOfType(plexstream.PlexStream.TYPE_AUDIO) + self.subtitleStream = self.part.getSelectedStreamOfType(plexstream.PlexStream.TYPE_SUBTITLE) + else: + util.WARN_LOG("Media does not contain a valid part") + + util.LOG("Choice media: {0} part:{1}".format(media, partIndex)) + for streamType in ("videoStream", "audioStream", "subtitleStream"): + attr = getattr(self, streamType) + if attr: + util.LOG("Choice {0}: {1}".format(streamType, repr(attr))) + else: + util.WARN_LOG("Could not create media choice for invalid media") + + def __str__(self): + return "direct playable={0} version={1}".format(self.isDirectPlayable, self.media) + + def __repr__(self): + return self.__str__() diff --git a/script.plexmod/lib/_included_packages/plexnet/mediadecisionengine.py b/script.plexmod/lib/_included_packages/plexnet/mediadecisionengine.py new file mode 100644 index 000000000..ffda4ef5a --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/mediadecisionengine.py @@ -0,0 +1,510 @@ +from __future__ import absolute_import +from . import mediachoice +from . import serverdecision +from . import plexapp +from . import util +import six +from six.moves import range + + +class MediaDecisionEngine(object): + proxyTypes = util.AttributeDict({ + 'NORMAL': 0, + 'LOCAL': 42, + 'CLOUD': 43 + }) + + def __init__(self): + self.softSubLanguages = None + + # TODO(schuyler): Do we need to allow this to be async? We may have to request + # the media again to fetch details, and we may need to make multiple requests to + # resolve an indirect. We can do it all async, we can block, or we can allow + # both. + + def chooseMedia(self, item, forceUpdate=False): + # If we've already evaluated this item, use our previous choice. + if not forceUpdate and item.mediaChoice is not None and item.mediaChoice.media is not None and not item.mediaChoice.media.isIndirect(): + return item.mediaChoice + + # See if we're missing media/stream details for this item. + if item.isLibraryItem() and item.isVideoItem() and len(item.media) > 0 and not item.media[0].hasStreams(): + # TODO(schuyler): Fetch the details + util.WARN_LOG("Can't make media choice, missing details") + + # Take a first pass through the media items to create an array of candidates + # that we'll evaluate more completely. If we find a forced item, we use it. + # If we find an indirect, we only keep a single candidate. + indirect = False + candidates = [] + maxResolution = item.settings.getMaxResolution(item.getQualityType()) + for mediaIndex in range(len(item.media)): + media = item.media[mediaIndex] + media.mediaIndex = mediaIndex + if media.isSelected(): + candidates = [] + candidates.append(media) + break + if media.isIndirect(): + # Only add indirect media if the resolution fits. We cannot + # exit early as the user may have selected media. + + indirect = True + if media.getVideoResolution() <= maxResolution: + candidates.append(media) + + elif media.isAccessible(): + # Only consider testing available media + candidates.append(media) + + # Only use the first indirect media item + if indirect and candidates: + candidates = candidates[0] + + # Make sure we have at least one valid item, regardless of availability + if len(candidates) == 0: + candidates.append(item.media[0]) + + # Now that we have an array of candidates, evaluate them completely. + choices = [] + for media in candidates: + choice = None + if media is not None: + if item.isVideoItem(): + choice = self.evaluateMediaVideo(item, media) + elif item.isMusicItem(): + choice = self.evaluateMediaMusic(item, media) + else: + choice = mediachoice.MediaChoice(media) + choices.append(choice) + item.mediaChoice = self.sortChoices(choices)[-1] + util.LOG("MDE: MediaChoice: {0}".format(item.mediaChoice)) + return item.mediaChoice + + def sortChoices(self, choices): + if choices is None: + return [] + + if len(choices) > 1: + self.sort(choices, "bitrate") + self.sort(choices, "audioChannels") + self.sort(choices, "audioDS") + self.sort(choices, "resolution") + self.sort(choices, "videoDS") + self.sort(choices, "directPlay") + self.sort(choices, self.higherResIfCapable) + self.sort(choices, self.cloudIfRemote) + + return choices + + def evaluateMediaVideo(self, item, media, partIndex=0): + # Resolve indirects before doing anything else. + if media.isIndirect(): + util.LOG("Resolve indirect media for {0}".format(item)) + media = media.resolveIndirect() + + choice = mediachoice.MediaChoice(media, partIndex) + server = item.getServer() + + if not media: + return choice + + choice.isSelected = media.isSelected() + choice.protocol = media.protocol("http") + + maxResolution = item.settings.getMaxResolution(item.getQualityType(), self.isSupported4k(media, choice.videoStream)) + maxBitrate = item.settings.getMaxBitrate(item.getQualityType()) + + choice.resolution = media.getVideoResolution() + if choice.resolution > maxResolution or media.bitrate.asInt() > maxBitrate: + choice.forceTranscode = True + + if choice.subtitleStream: + choice.subtitleDecision = self.evaluateSubtitles(choice.subtitleStream) + choice.hasBurnedInSubtitles = (choice.subtitleDecision != choice.SUBTITLES_SOFT_DP and choice.subtitleDecision != choice.SUBTITLES_SOFT_ANY) + else: + choice.hasBurnedInSubtitles = False + + # For evaluation purposes, we only care about the first part + part = media.parts[partIndex] + if not part: + return choice + + # Although PMS has already told us which streams are selected, we can't + # necessarily tell the video player which streams we want. So we need to + # iterate over the streams and see if there are any red flags that would + # prevent direct play. If there are multiple video streams, we're hosed. + # For audio streams, we have a fighting chance if the selected stream can + # be selected by language, but we need to be careful about guessing which + # audio stream the Roku will pick for a given language. + + numVideoStreams = 0 + problematicAudioStream = False + + if part.get('hasChapterVideoStream').asBool(): + numVideoStreams = 1 + + for stream in part.streams: + streamType = stream.streamType.asInt() + if streamType == stream.TYPE_VIDEO: + numVideoStreams = numVideoStreams + 1 + + if stream.codec == "h264" or ( + stream.codec == "hevc" and item.settings.getPreference("allow_hevc", True) + ) or ( + stream.codec == "av1" and item.settings.getPreference("allow_av1", False) + ) or ( + stream.codec == "vp9" and item.settings.getGlobal("vp9Support") + ): + choice.sorts.videoDS = 1 + + # Special cases to force direct play + forceDirectPlay = False + if choice.protocol == "hls": + util.LOG("MDE: Assuming HLS is direct playable") + forceDirectPlay = True + elif not server.supportsVideoTranscoding: + # See if we can use another server to transcode, otherwise force direct play + transcodeServer = item.getTranscodeServer(True, "video") + if not transcodeServer or not transcodeServer.supportsVideoTranscoding: + util.LOG("MDE: force direct play because the server does not support video transcoding") + forceDirectPlay = True + + # See if we found any red flags based on the streams. Otherwise, go ahead + # with our codec checks. + + if forceDirectPlay: + # Consider the choice DP, but continue to allow the + # choice to have the sorts set properly. + choice.isDirectPlayable = True + elif choice.hasBurnedInSubtitles: + util.LOG("MDE: Need to burn in subtitles") + elif choice.protocol != "http": + util.LOG("MDE: " + choice.protocol + " not supported") + # elif numVideoStreams > 1: + # util.LOG("MDE: Multiple video streams, won't try to direct play") + elif problematicAudioStream: + util.LOG("MDE: Problematic AAC stream with more than 2 channels prevents direct play") + elif self.canDirectPlay(item, choice): + choice.isDirectPlayable = True + elif item.isMediaSynthesized: + util.LOG("MDE: assuming synthesized media can direct play") + choice.isDirectPlayable = True + + # Check for a server decision. This is authority as it's the only playback type + # the server will allow. This will also support forcing direct play, overriding + # only our local MDE checks based on the user pref, and only if the server + # agrees. + decision = part.get("decision") + if decision: + if decision != serverdecision.ServerDecision.DECISION_DIRECT_PLAY: + util.LOG("MDE: Server has decided this cannot direct play") + choice.isDirectPlayable = False + else: + util.LOG("MDE: Server has allowed direct play") + choice.isDirectPlayable = True + + # Setup sorts + if choice.videoStream is not None: + choice.sorts.bitrate = choice.videoStream.bitrate.asInt() + elif choice.media is not None: + choice.sorts.bitrate = choice.media.bitrate.asInt() + else: + choice.sorts.bitrate = 0 + + if choice.audioStream is not None: + choice.sorts.audioChannels = choice.audioStream.channels.asInt() + elif choice.media is not None: + choice.sorts.audioChannels = choice.media.audioChannels.asInt() + else: + choice.sorts.audioChannels = 0 + + AC3Cond = item.settings.getPreference("audio_force_ac3_cond", 'never') + + if AC3Cond != 'never': + allowed = ["ac3"] + if item.settings.getPreference("audio_ac3dts", False): + allowed.append("dca") + + if AC3Cond == 'always' and choice.audioStream.codec not in allowed: + util.LOG("MDE: Codec {} can't be direct played due to user settings".format(choice.audioStream.codec)) + choice.isDirectPlayable = False + + elif AC3Cond in ('2', '5'): + ch = int(AC3Cond) + ach = choice.sorts.audioChannels + + # got AC3/DTS but channels don't match + if choice.audioStream.codec in allowed and ach > ch: + util.LOG("MDE: {}-channel AC3/DTS can't be direct played due " + "to user settings ({} ch)".format(ach, ch)) + choice.isDirectPlayable = False + # other codec and channels don't match + elif ach > ch: + util.LOG("MDE: {}-channel {} can't be direct played due to " + "user settings ({} ch)".format(ach, choice.audioStream.codec, ch)) + choice.isDirectPlayable = False + + choice.sorts.videoDS = not ( + choice.sorts.videoDS is None or choice.forceTranscode is True) and choice.sorts.videoDS or 0 + choice.sorts.resolution = choice.resolution + + # Server properties probably don't need to be associated with each choice + choice.sorts.canTranscode = server.supportsVideoTranscoding and 1 or 0 + choice.sorts.canRemuxOnly = server.supportsVideoRemuxOnly and 1 or 0 + choice.sorts.directPlay = (choice.isDirectPlayable is True and choice.forceTranscode is not True) and 1 or 0 + choice.sorts.proxyType = choice.media.proxyType and choice.media.proxyType or self.proxyTypes.NORMAL + + return choice + + def canDirectPlay(self, item, choice): + maxResolution = item.settings.getMaxResolution(item.getQualityType(), self.isSupported4k(choice.media, choice.videoStream)) + height = choice.media.getVideoResolution() + if height > maxResolution: + util.LOG("MDE: (DP) Video height is greater than max allowed: {0} > {1}".format(height, maxResolution)) + if height > 1088 and item.settings.getPreference("allow_4k", True): + util.LOG("MDE: (DP) Unsupported 4k media") + return False + + maxBitrate = item.settings.getMaxBitrate(item.getQualityType()) + bitrate = choice.media.bitrate.asInt() + if bitrate > maxBitrate: + util.LOG("MDE: (DP) Video bitrate is greater than the allowed max: {0} > {1}".format(bitrate, maxBitrate)) + return False + + if choice.videoStream is None: + util.ERROR_LOG("MDE: (DP) No video stream") + return True + + if not item.settings.getGlobal("supports1080p60"): + videoFrameRate = choice.videoStream.asInt() + if videoFrameRate > 30 and height >= 1080: + util.LOG("MDE: (DP) Frame rate is not supported for resolution: {0}@{1}".format(height, videoFrameRate)) + return False + + if choice.videoStream.codec == "hevc" and not item.settings.getPreference("allow_hevc", True): + util.LOG("MDE: (DP) Codec is HEVC, which is disabled") + return False + + if choice.videoStream.codec == "av1" and not item.settings.getPreference("allow_av1", False): + util.LOG("MDE: (DP) Codec is AV1, which is disabled") + return False + + return True + + # container = choice.media.get('container') + # videoCodec = choice.videoStream.codec + # if choice.audioStream is None: + # audioCodec = None + # numChannels = 0 + # else: + # audioCodec = choice.audioStream.codec + # numChannels = choice.audioStream.channels.asInt() + + # Formats: https://support.roku.com/hc/en-us/articles/208754908-Roku-Media-Player-Playing-your-personal-videos-music-photos + # All Models: H.264/AVC (MKV, MP4, MOV), + # Roku 4 only: H.265/HEVC (MKV, MP4, MOV); VP9 (.MKV) + + # if True: # container in ("mp4", "mov", "m4v", "mkv"): + # util.LOG("MDE: {0} container looks OK, checking streams".format(container)) + + # isHEVC = videoCodec == "hevc" and item.settings.getPreference("allow_hevc", False) + # isVP9 = videoCodec == "vp9" and container == "mkv" and item.settings.getGlobal("vp9Support") + + # if videoCodec != "h264" and videoCodec != "mpeg4" and not isHEVC and not isVP9: + # util.LOG("MDE: Unsupported video codec: {0}".format(videoCodec)) + # return False + + # # TODO(schuyler): Fix ref frames check. It's more nuanced than this. + # if choice.videoStream.refFrames.asInt() > 8: + # util.LOG("MDE: Too many ref frames: {0}".format(choice.videoStream.refFrames)) + # return False + + # # HEVC supports a bitDepth of 10, otherwise 8 is the limit + # if choice.videoStream.bitDepth.asInt() > (isHEVC and 10 or 8): + # util.LOG("MDE: Bit depth too high: {0}".format(choice.videoStream.bitDepth)) + # return False + + # # We shouldn't have to whitelist particular audio codecs, we can just + # # check to see if the Roku can decode this codec with the number of channels. + # if not item.settings.supportsAudioStream(audioCodec, numChannels): + # util.LOG("MDE: Unsupported audio track: {0} ({1} channels)".format(audioCodec, numChannels)) + # return False + + # # # TODO(schuyler): We've reported this to Roku, they may fix it. If/when + # # # they do, we should move this behind a firmware version check. + # # if container == "mkv" and choice.videoStream.headerStripping.asBool() and audioCodec == "ac3": + # # util.ERROR_LOG("MDE: Header stripping with AC3 audio") + # # return False + + # # Those were our problems, everything else should be OK. + # return True + # else: + # util.LOG("MDE: Unsupported container: {0}".format(container)) + + # return False + + def evaluateSubtitles(self, stream): + if util.INTERFACE.getPreference("burn_subtitles") == "always": + # If the user prefers them burned, always burn + return mediachoice.MediaChoice.SUBTITLES_BURN + # elif stream.codec != "srt": + # # We only support soft subtitles for SRT. Anything else has to use the + # # transcoder, and we defer to it on whether the subs will have to be + # # burned or can be converted to SRT and muxed. + + # return mediachoice.MediaChoice.SUBTITLES_DEFAULT + elif stream.key is None: + # Embedded subs don't have keys and can only be direct played + result = mediachoice.MediaChoice.SUBTITLES_SOFT_DP + else: + # Sidecar subs can be direct played or used alongside a transcode + result = mediachoice.MediaChoice.SUBTITLES_SOFT_ANY + + # # TODO(schuyler) If Roku adds support for non-Latin characters, remove + # # this hackery. To the extent that we continue using this hackery, it + # # seems that the Roku requires UTF-8 subtitles but only supports characters + # # from Windows-1252. This should be the full set of languages that are + # # completely representable in Windows-1252. PMS should specifically be + # # returning ISO 639-2/B language codes. + # # Update: Roku has added support for additional characters, but still only + # # Latin characters. We can now basically support anything from the various + # # ISO-8859 character sets, but nothing non-Latin. + + # if not self.softSubLanguages: + # self.softSubLanguages = frozenset(( + # 'afr', + # 'alb', + # 'baq', + # 'bre', + # 'cat', + # 'cze', + # 'dan', + # 'dut', + # 'eng', + # 'epo', + # 'est', + # 'fao', + # 'fin', + # 'fre', + # 'ger', + # 'gla', + # 'gle', + # 'glg', + # 'hrv', + # 'hun', + # 'ice', + # 'ita', + # 'lat', + # 'lav', + # 'lit', + # 'ltz', + # 'may', + # 'mlt', + # 'nno', + # 'nob', + # 'nor', + # 'oci', + # 'pol', + # 'por', + # 'roh', + # 'rum', + # 'slo', + # 'slv', + # 'spa', + # 'srd', + # 'swa', + # 'swe', + # 'tur', + # 'vie', + # 'wel', + # 'wln' + # )) + + # if not (stream.languageCode or 'eng') in self.softSubLanguages: + # # If the language is unsupported,: we need to force burning + # result = mediachoice.MediaChoice.SUBTITLES_BURN + + return result + + def evaluateMediaMusic(self, item, media): + # Resolve indirects before doing anything else. + if media.isIndirect(): + util.LOG("Resolve indirect media for {0}".format(item)) + media = media.resolveIndirect() + + choice = mediachoice.MediaChoice(media) + if media is None: + return choice + + # Verify the server supports audio transcoding, otherwise force direct play + if not item.getServer().supportsAudioTranscoding: + util.LOG("MDE: force direct play because the server does not support audio transcoding") + choice.isDirectPlayable = True + return choice + + # See if this part has a server decision to transcode and obey it + if choice.part and choice.part.get( + "decision", serverdecision.ServerDecision.DECISION_DIRECT_PLAY + ) != serverdecision.ServerDecision.DECISION_DIRECT_PLAY: + util.WARN_LOG("MDE: Server has decided this cannot direct play") + return choice + + # Verify the codec and container are compatible + codec = media.audioCodec + container = media.get('container') + canPlayCodec = item.settings.supportsAudioStream(codec, media.audioChannels.asInt()) + canPlayContainer = (codec == container) or True # (container in ("mp4", "mka", "mkv")) + + choice.isDirectPlayable = (canPlayCodec and canPlayContainer) + if choice.isDirectPlayable: + # Inspect the audio stream attributes if the codec/container can direct + # play. For now we only need to verify the sample rate. + + if choice.audioStream is not None and choice.audioStream.samplingRate.asInt() >= 192000: + util.LOG("MDE: sampling rate is not compatible") + choice.isDirectPlayable = False + else: + util.LOG("MDE: container or codec is incompatible") + + return choice + + # Simple Quick sort function modeled after roku sdk function + def sort(self, choices, key=None): + if not isinstance(choices, list): + return + + if key is None: + choices.sort() + elif isinstance(key, six.string_types): + choices.sort(key=lambda x: getattr(x.media, key)) + elif hasattr(key, '__call__'): + choices.sort(key=key) + + def higherResIfCapable(self, choice): + if choice.media is not None: + server = choice.media.getServer() + if server.supportsVideoTranscoding and not server.supportsVideoRemuxOnly and (choice.sorts.directPlay == 1 or choice.sorts.videoDS == 1): + return util.validInt(choice.sorts.resolution) + + return 0 + + def cloudIfRemote(self, choice): + if choice.media is not None and choice.media.getServer().isLocalConnection() and choice.media.proxyType != self.proxyTypes.CLOUD: + return 1 + + return 0 + + def isSupported4k(self, media, videoStream): + if videoStream is None or not util.INTERFACE.getPreference("allow_4k", True): + return False + + # # Roku 4 only: H.265/HEVC (MKV, MP4, MOV); VP9 (.MKV) + # if media.get('container') in ("mp4", "mov", "m4v", "mkv"): + # isHEVC = (videoStream.codec == "hevc" and util.INTERFACE.getPreference("allow_hevc")) + # isVP9 = (videoStream.codec == "vp9" and media.get('container') == "mkv" and util.INTERFACE.getGlobal("vp9Support")) + # return (isHEVC or isVP9) + + # return False + + return True diff --git a/script.plexmod/lib/_included_packages/plexnet/myplex.py b/script.plexmod/lib/_included_packages/plexnet/myplex.py new file mode 100644 index 000000000..94b88e962 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/myplex.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from . import util +from . import http +from threading import Thread +from xml.etree import ElementTree +import time + +from . import exceptions + +from . import video +from . import audio +from . import photo +from . import plexobjects + +video, audio, photo # Hides warning message + + +class PinLogin(object): + INIT = 'https://plex.tv/pins.xml' + POLL = 'https://plex.tv/pins/{0}.xml' + POLL_INTERVAL = 1 + + def __init__(self, callback=None): + self._callback = callback + self.id = None + self.pin = None + self.authenticationToken = None + self._finished = False + self._abort = False + self._expired = False + self._init() + + def _init(self): + response = http.POST(self.INIT) + if response.status_code != http.codes.created: + codename = http.status_codes.get(response.status_code)[0] + raise exceptions.BadRequest('({0}) {1}'.format(response.status_code, codename)) + data = ElementTree.fromstring(response.text.encode('utf-8')) + self.pin = data.find('code').text + self.id = data.find('id').text + + def _poll(self): + try: + start = time.time() + while not self._abort and time.time() - start < 300: + try: + response = http.GET(self.POLL.format(self.id)) + except Exception as e: + util.ERROR('PinLogin connection error: {0}'.format(e.__class__), err=e) + time.sleep(self.POLL_INTERVAL) + continue + + if response.status_code != http.codes.ok: + self._expired = True + break + try: + data = ElementTree.fromstring(response.text.encode('utf-8')) + except Exception as e: + util.ERROR('PinLogin data error: {0}'.format(e.__class__), err=e) + time.sleep(self.POLL_INTERVAL) + continue + + token = data.find('auth_token').text + if token: + self.authenticationToken = token + break + time.sleep(self.POLL_INTERVAL) + + if self._callback: + self._callback(self.authenticationToken) + finally: + self._finished = True + + def finished(self): + return self._finished + + def expired(self): + return self._expired + + def startTokenPolling(self): + t = Thread(target=self._poll, name='PIN-LOGIN:Token-Poll') + t.start() + return t + + def waitForToken(self): + t = self.startTokenPolling() + t.join() + return self.authenticationToken + + def abort(self): + self._abort = True diff --git a/script.plexmod/lib/_included_packages/plexnet/myplexaccount.py b/script.plexmod/lib/_included_packages/plexnet/myplexaccount.py new file mode 100644 index 000000000..6eb1d65bb --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/myplexaccount.py @@ -0,0 +1,323 @@ +from __future__ import absolute_import +import json +import time +import hashlib +from xml.etree import ElementTree + +from . import plexapp +from . import myplexrequest +from . import locks +from . import callback +from . import asyncadapter + +from . import util + +ACCOUNT = None + + +class HomeUser(util.AttributeDict): + pass + + +class MyPlexAccount(object): + def __init__(self): + # Strings + self.ID = None + self.title = None + self.username = None + self.thumb = None + self.email = None + self.authToken = None + self.pin = None + self.thumb = None + + # Booleans + self.isAuthenticated = util.INTERFACE.getPreference('auto_signin', False) + self.isSignedIn = False + self.isOffline = False + self.isExpired = False + self.isPlexPass = False + self.isManaged = False + self.isSecure = False + self.hasQueue = False + + self.isAdmin = False + self.switchUser = False + + self.adminHasPlexPass = False + + self.lastHomeUserUpdate = None + self.homeUsers = [] + + def init(self): + self.loadState() + + def saveState(self): + obj = { + 'ID': self.ID, + 'title': self.title, + 'username': self.username, + 'email': self.email, + 'authToken': self.authToken, + 'pin': self.pin, + 'isPlexPass': self.isPlexPass, + 'isManaged': self.isManaged, + 'isAdmin': self.isAdmin, + 'isSecure': self.isSecure, + 'adminHasPlexPass': self.adminHasPlexPass, + 'thumb': self.thumb + } + + util.INTERFACE.setRegistry("MyPlexAccount", json.dumps(obj), "myplex") + + def loadState(self): + # Look for the new JSON serialization. If it's not there, look for the + # old token and Plex Pass values. + + util.APP.addInitializer("myplex") + + jstring = util.INTERFACE.getRegistry("MyPlexAccount", None, "myplex") + + if jstring: + try: + obj = json.loads(jstring) + except: + util.ERROR() + obj = None + + if obj: + self.ID = obj.get('ID') or self.ID + self.title = obj.get('title') or self.title + self.username = obj.get('username') or self.username + self.email = obj.get('email') or self.email + self.authToken = obj.get('authToken') or self.authToken + self.pin = obj.get('pin') or self.pin + self.isPlexPass = obj.get('isPlexPass') or self.isPlexPass + self.isManaged = obj.get('isManaged') or self.isManaged + self.isAdmin = obj.get('isAdmin') or self.isAdmin + self.isSecure = obj.get('isSecure') or self.isSecure + self.isProtected = bool(obj.get('pin')) + self.adminHasPlexPass = obj.get('adminHasPlexPass') or self.adminHasPlexPass + self.thumb = obj.get('thumb') + + def verifyAccount(self): + if self.authToken: + request = myplexrequest.MyPlexRequest("/users/account") + context = request.createRequestContext("account", callback.Callable(self.onAccountResponse), + timeout=util.LONG_TIMEOUT) + util.APP.startRequest(request, context) + else: + util.APP.clearInitializer("myplex") + + def logState(self): + util.LOG("Authenticated as {0}:{1}".format(self.ID, repr(self.title))) + util.LOG("SignedIn: {0}".format(self.isSignedIn)) + util.LOG("Offline: {0}".format(self.isOffline)) + util.LOG("Authenticated: {0}".format(self.isAuthenticated)) + util.LOG("PlexPass: {0}".format(self.isPlexPass)) + util.LOG("Managed: {0}".format(self.isManaged)) + util.LOG("Protected: {0}".format(self.isProtected)) + util.LOG("Admin: {0}".format(self.isAdmin)) + util.LOG("AdminPlexPass: {0}".format(self.adminHasPlexPass)) + + def onAccountResponse(self, request, response, context): + oldId = self.ID + + if response.isSuccess(): + data = response.getBodyXml() + + # The user is signed in + self.isSignedIn = True + self.isOffline = False + self.ID = data.attrib.get('id') + self.title = data.attrib.get('title') + self.username = data.attrib.get('username') + self.email = data.attrib.get('email') + self.thumb = data.attrib.get('thumb') + self.authToken = data.attrib.get('authenticationToken') + self.isPlexPass = (data.find('subscription') is not None and data.find('subscription').attrib.get('active') == '1') + self.isManaged = data.attrib.get('restricted') == '1' + self.isSecure = data.attrib.get('secure') == '1' + self.hasQueue = bool(data.attrib.get('queueEmail')) + + # PIN + if data.attrib.get('pin'): + self.pin = data.attrib.get('pin') + else: + self.pin = None + self.isProtected = bool(self.pin) + + # update the list of users in the home + self.updateHomeUsers() + + # set admin attribute for the user + self.isAdmin = False + if self.homeUsers: + for user in self.homeUsers: + if self.ID == user.id: + self.isAdmin = str(user.admin) == "1" + break + + if self.isAdmin and self.isPlexPass: + self.adminHasPlexPass = True + + # consider a single, unprotected user authenticated + if not self.isAuthenticated and not self.isProtected and len(self.homeUsers) <= 1: + self.isAuthenticated = True + + self.logState() + + self.saveState() + util.MANAGER.publish() + + if oldId != self.ID or self.switchUser: + util.DEBUG_LOG("User changed, deferring refresh resources (force=False)") + else: + util.DEBUG_LOG("User selected, refreshing resources (force=False)") + plexapp.refreshResources() + + elif response.getStatus() >= 400 and response.getStatus() < 500: + # The user is specifically unauthorized, clear everything + util.WARN_LOG("Sign Out: User is unauthorized") + self.signOut(True) + else: + # Unexpected error, keep using whatever we read from the registry + util.WARN_LOG("Unexpected response from plex.tv ({0}), switching to OFFLINE mode".format(response.getStatus())) + self.logState() + self.isOffline = True + # consider a single, unprotected user authenticated + if not self.isAuthenticated and not self.isProtected: + self.isAuthenticated = True + + util.APP.clearInitializer("myplex") + # Logger().UpdateSyslogHeader() # TODO: ------------------------------------------------------------------------------------------------------IMPLEMENT + + if oldId != self.ID or self.switchUser: + self.switchUser = None + util.APP.trigger("change:user", account=self, reallyChanged=oldId != self.ID) + + util.APP.trigger("account:response") + + def signOut(self, expired=False): + # Strings + self.ID = None + self.title = None + self.username = None + self.email = None + self.authToken = None + self.pin = None + self.lastHomeUserUpdate = None + + # Booleans + self.isSignedIn = False + self.isPlexPass = False + self.adminHasPlexPass = False + self.isManaged = False + self.isSecure = False + self.isExpired = expired + + # Clear the saved resources + util.INTERFACE.clearRegistry("mpaResources", "xml_cache") + + # Remove all saved servers + plexapp.SERVERMANAGER.clearServers() + + # Enable the welcome screen again + util.INTERFACE.setPreference("show_welcome", True) + + util.APP.trigger("change:user", account=self, reallyChanged=True) + + self.saveState() + + def hasPlexPass(self): + return self.isPlexPass or self.adminHasPlexPass + + def validateToken(self, token, switchUser=False): + self.authToken = token + self.switchUser = switchUser + + request = myplexrequest.MyPlexRequest("/users/sign_in.xml") + context = request.createRequestContext("sign_in", callback.Callable(self.onAccountResponse), + timeout=util.LONG_TIMEOUT) + if self.isOffline: + context.timeout = self.isOffline and asyncadapter.AsyncTimeout(1).setConnectTimeout(1) + util.APP.startRequest(request, context, {}) + + def refreshAccount(self): + if not self.authToken: + return + self.validateToken(self.authToken, False) + + def updateHomeUsers(self): + # Ignore request and clear any home users we are not signed in + if not self.isSignedIn: + self.homeUsers = [] + if self.isOffline: + self.homeUsers.append(MyPlexAccount()) + + self.lastHomeUserUpdate = None + return + + # Cache home users for 60 seconds, mainly to stop back to back tests + epoch = time.time() + if not self.lastHomeUserUpdate: + self.lastHomeUserUpdate = epoch + elif self.lastHomeUserUpdate + 60 > epoch: + util.DEBUG_LOG("Skipping home user update (updated {0} seconds ago)".format(epoch - self.lastHomeUserUpdate)) + return + + req = myplexrequest.MyPlexRequest("/api/home/users") + xml = req.getToStringWithTimeout(seconds=util.LONG_TIMEOUT) + data = ElementTree.fromstring(xml) + if data.attrib.get('size') and data.find('User') is not None: + self.homeUsers = [] + for user in data.findall('User'): + homeUser = HomeUser(user.attrib) + homeUser.isAdmin = homeUser.admin == "1" + homeUser.isManaged = homeUser.restricted == "1" + homeUser.isProtected = homeUser.protected == "1" + self.homeUsers.append(homeUser) + + self.lastHomeUserUpdate = epoch + + util.LOG("home users: {0}".format(self.homeUsers)) + + def switchHomeUser(self, userId, pin=''): + if userId == self.ID and self.isAuthenticated: + return True + + # Offline support + if self.isOffline: + hashed = 'NONE' + if pin and self.authToken: + hashed = hashlib.sha256(pin + self.authToken).digest() + + if not self.isProtected or self.isAuthenticated or hashed == (self.pin or ""): + util.DEBUG_LOG("OFFLINE access granted") + self.isAuthenticated = True + self.validateToken(self.authToken, True) + return True + else: + # build path and post to myplex to swith the user + path = '/api/home/users/{0}/switch'.format(userId) + req = myplexrequest.MyPlexRequest(path) + xml = req.postToStringWithTimeout({'pin': pin}, seconds=util.LONG_TIMEOUT) + try: + data = ElementTree.fromstring(xml) + except: + return False + + if data.attrib.get('authenticationToken'): + self.isAuthenticated = True + # validate the token (trigger change:user) on user change or channel startup + if userId != self.ID or not locks.LOCKS.isLocked("idleLock"): + self.validateToken(data.attrib.get('authenticationToken'), True) + return True + + return False + + def isActive(self): + return self.isSignedIn or self.isOffline + + +ACCOUNT = MyPlexAccount() diff --git a/script.plexmod/lib/_included_packages/plexnet/myplexmanager.py b/script.plexmod/lib/_included_packages/plexnet/myplexmanager.py new file mode 100644 index 000000000..72c2ebb9c --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/myplexmanager.py @@ -0,0 +1,79 @@ +from __future__ import absolute_import +from xml.etree import ElementTree +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error + +from . import plexapp +from . import plexconnection +from . import plexserver +from . import myplexrequest +from . import callback +from . import util + + +class MyPlexManager(object): + def publish(self): + util.LOG('MyPlexManager().publish() - NOT IMPLEMENTED') + return # TODO: ----------------------------------------------------------------------------------------------------------------------------- IMPLEMENT? + request = myplexrequest.MyPlexRequest("/devices/" + util.INTERFACE.getGlobal("clientIdentifier")) + context = request.createRequestContext("publish") + + addrs = util.INTERFACE.getGlobal("roDeviceInfo").getIPAddrs() + + for iface in addrs: + request.addParam(six.moves.urllib.parse.quote("Connection[][uri]"), "http://{0):8324".format(addrs[iface])) + + util.APP.startRequest(request, context, "_method=PUT") + + def refreshResources(self, force=False): + util.LOG('MyPlexManager().refreshResources() - Force: {}'.format(force)) + if force: + plexapp.SERVERMANAGER.resetLastTest() + + request = myplexrequest.MyPlexRequest("/pms/resources") + context = request.createRequestContext("resources", callback.Callable(self.onResourcesResponse), + timeout=util.LONG_TIMEOUT) + + if plexapp.ACCOUNT.isSecure: + request.addParam("includeHttps", "1") + + util.APP.startRequest(request, context) + + def onResourcesResponse(self, request, response, context): + servers = [] + + response.parseResponse() + + # Save the last successful response to cache + if response.isSuccess() and response.event: + util.INTERFACE.setRegistry("mpaResources", response.event.text.encode('utf-8'), "xml_cache") + util.DEBUG_LOG("Saved resources response to registry") + # Load the last successful response from cache + elif util.INTERFACE.getRegistry("mpaResources", None, "xml_cache"): + data = ElementTree.fromstring(util.INTERFACE.getRegistry("mpaResources", None, "xml_cache")) + response.parseFakeXMLResponse(data) + util.DEBUG_LOG("Using cached resources") + + if response.container: + for resource in response.container: + util.DEBUG_LOG( + "Parsed resource from plex.tv: type:{0} clientIdentifier:{1} name:{2} product:{3} provides:{4}".format( + resource.type, + resource.clientIdentifier, + resource.name.encode('utf-8'), + resource.product.encode('utf-8'), + resource.provides.encode('utf-8') + ) + ) + + for conn in resource.connections: + util.DEBUG_LOG(' {0}'.format(conn)) + + if 'server' in resource.provides: + server = plexserver.createPlexServerForResource(resource) + util.DEBUG_LOG(' {0}'.format(server)) + servers.append(server) + + plexapp.SERVERMANAGER.updateFromConnectionType(servers, plexconnection.PlexConnection.SOURCE_MYPLEX) + + +MANAGER = MyPlexManager() diff --git a/script.plexmod/lib/_included_packages/plexnet/myplexrequest.py b/script.plexmod/lib/_included_packages/plexnet/myplexrequest.py new file mode 100644 index 000000000..5f560afe9 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/myplexrequest.py @@ -0,0 +1,13 @@ +# We don't particularly need a class definition here (yet?), it's just a +# PlexRequest where the server is fixed. +from __future__ import absolute_import +from . import plexrequest + + +class MyPlexRequest(plexrequest.PlexServerRequest): + def __init__(self, path): + from . import myplexserver + plexrequest.PlexServerRequest.__init__(self, myplexserver.MyPlexServer(), path) + + # Make sure we're always getting XML + self.addHeader("Accept", "application/xml") diff --git a/script.plexmod/lib/_included_packages/plexnet/myplexserver.py b/script.plexmod/lib/_included_packages/plexnet/myplexserver.py new file mode 100644 index 000000000..aad8e3f03 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/myplexserver.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import +from . import plexapp +from . import plexconnection +from . import plexserver +from . import plexresource +from . import plexservermanager + + +class MyPlexServer(plexserver.PlexServer): + TYPE = 'MYPLEXSERVER' + + def __init__(self): + plexserver.PlexServer.__init__(self) + self.uuid = 'myplex' + self.name = 'plex.tv' + conn = plexconnection.PlexConnection(plexresource.ResourceConnection.SOURCE_MYPLEX, "https://plex.tv", False, None) + self.connections.append(conn) + self.activeConnection = conn + + def getToken(self): + return plexapp.ACCOUNT.authToken + + def buildUrl(self, path, includeToken=False): + if "://node.plexapp.com" in path: + # Locate the best fit server that supports channels, otherwise we'll + # continue to use the node urls. Service code between the node and + # PMS differs sometimes, so it's a toss up which one is actually + # more accurate. Either way, we try to offload work from the node. + + server = plexservermanager.MANAGER.getChannelServer() + if server: + url = server.swizzleUrl(path, includeToken) + if url: + return url + + return plexserver.PlexServer.buildUrl(self, path, includeToken) diff --git a/script.plexmod/lib/_included_packages/plexnet/netif/__init__.py b/script.plexmod/lib/_included_packages/plexnet/netif/__init__.py new file mode 100644 index 000000000..03c29ea03 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/netif/__init__.py @@ -0,0 +1,217 @@ +from __future__ import absolute_import +import socket +import struct +import os +from six.moves import range + + +class Interface: + def __init__(self): + self.name = '' + self.ip = '' + self.mask = '' + + @property + def broadcast(self): + if self.name == 'FALLBACK': return '' + if not self.ip or not self.mask: return None + return calcBroadcast(self.ip,self.mask) + + +def getInterfaces(): + if os.name == "nt": + try: + ret = _getInterfacesWin() + if ret: + return ret + except: + raise + + try: + return _getInterfacesWinPS() + except: + pass + + try: + return _getInterfaces() + except: + pass + + try: + return _getInterfacesBSD() + except: + pass + + i = Interface() + i.name = 'FALLBACK' + return [i] + + +def _getInterfaces(): + vals = all_interfaces() + interfaces = [] + for name,ip in vals: + i = Interface() + i.name = name.decode() + i.ip = ip + try: + mask = getSubnetMask(i.name) + i.mask = mask + except: + i.mask = '' + interfaces.append(i) + return interfaces + + +def _getInterfacesBSD(): + #name flags family address netmask + interfaces = [] + from . import getifaddrs + for info in getifaddrs.getifaddrs(): + if info.family == 2: + i = Interface() + i.name = info.name + i.ip = info.address + i.mask = info.netmask + interfaces.append(i) + return interfaces + + +def _getInterfacesWin(): + from . import ipconfig + interfaces = [] + adapters = ipconfig.parse() + for a in adapters: + if not 'IPv4 Address' in a: continue + if not 'Subnet Mask' in a: continue + i = Interface() + i.name = a.get('name','UNKNOWN') + i.ip = a['IPv4 Address'] + i.mask = a['Subnet Mask'] + interfaces.append(i) + return interfaces + + +def _getInterfacesWinPS(): + from . import winpsif + interfaces = [] + + for (name, ip, subnet) in winpsif.getInterfaces(): + i = Interface() + i.name = name + i.ip = ip + i.mask = subnet + interfaces.append(i) + + return interfaces + + +def all_interfaces(): + import sys + import array + import fcntl + + is_64bits = sys.maxsize > 2**32 + struct_size = 40 if is_64bits else 32 + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + max_possible = 8 # initial value + while True: + bytes = max_possible * struct_size + names = array.array('B', b'\0' * bytes) + outbytes = struct.unpack('iL', fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack('iL', bytes, names.buffer_info()[0]) + ))[0] + if outbytes == bytes: + max_possible *= 2 + else: + break + namestr = names.tobytes() + return [(namestr[i:i+16].split(b'\0', 1)[0], + socket.inet_ntoa(namestr[i+20:i+24])) + for i in range(0, outbytes, struct_size)] + +def getSubnetMask(name): + import fcntl + return socket.inet_ntoa(fcntl.ioctl(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), 35099, struct.pack('256s', name))[20:24]) + +def calcIPValue(ipaddr): + """ + Calculates the binary + value of the ip addresse + """ + ipaddr = ipaddr.split('.') + value = 0 + for i in range(len(ipaddr)): + value = value | (int(ipaddr[i]) << ( 8*(3-i) )) + return value + +def calcIPNotation(value): + """ + Calculates the notation + of the ip addresse given its value + """ + notat = [] + for i in range(4): + shift = 255 << ( 8*(3-i) ) + part = value & shift + part = part >> ( 8*(3-i) ) + notat.append(str(part)) + notat = '.'.join(notat) + return notat + +def calcSubnet(cidr): + """ + Calculates the Subnet + based on the CIDR + """ + subn = 4294967295 << (32-cidr) # 4294967295 = all bits set to 1 + subn = subn % 4294967296 # round it back to be 4 bytes + subn = calcIPNotation(subn) + return subn + +def calcCIDR(subnet): + """ + Calculates the CIDR + based on the SUbnet + """ + cidr = 0 + subnet = calcIPValue(subnet) + while subnet != 0: + subnet = subnet << 1 + subnet = subnet % 4294967296 + cidr += 1 + return cidr + +def calcNetpart(ipaddr,subnet): + ipaddr = calcIPValue(ipaddr) + subnet = calcIPValue(subnet) + netpart = ipaddr & subnet + netpart = calcIPNotation(netpart) + return netpart + +def calcMacpart(subnet): + macpart = ~calcIPValue(subnet) + macpart = calcIPNotation(macpart) + return macpart + +def calcBroadcast(ipaddr,subnet): + netpart = calcNetpart(ipaddr,subnet) + macpart = calcMacpart(subnet) + netpart = calcIPValue(netpart) + macpart = calcIPValue(macpart) + broadcast = netpart | macpart + broadcast = calcIPNotation(broadcast) + return broadcast + +def calcDefaultGate(ipaddr,subnet): + defaultgw = calcNetpart(ipaddr,subnet) + defaultgw = calcIPValue(defaultgw) + 1 + defaultgw = calcIPNotation(defaultgw) + return defaultgw + +def calcHostNum(subnet): + macpart = calcMacpart(subnet) + hostnum = calcIPValue(macpart) - 1 + return hostnum \ No newline at end of file diff --git a/script.plexmod/lib/_included_packages/plexnet/netif/getifaddrs.py b/script.plexmod/lib/_included_packages/plexnet/netif/getifaddrs.py new file mode 100644 index 000000000..16f227bd9 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/netif/getifaddrs.py @@ -0,0 +1,192 @@ +""" +Wrapper for getifaddrs(3). +""" + +from __future__ import absolute_import +from __future__ import print_function +import socket +import sys + +from collections import namedtuple +from ctypes import * +from six.moves import map + +class sockaddr_in(Structure): + _fields_ = [ + ('sin_len', c_uint8), + ('sin_family', c_uint8), + ('sin_port', c_uint16), + ('sin_addr', c_uint8 * 4), + ('sin_zero', c_uint8 * 8) + ] + + def __str__(self): + try: + assert self.sin_len >= sizeof(sockaddr_in) + data = ''.join(map(chr, self.sin_addr)) + return socket.inet_ntop(socket.AF_INET, data) + except: + return '' + +class sockaddr_in6(Structure): + _fields_ = [ + ('sin6_len', c_uint8), + ('sin6_family', c_uint8), + ('sin6_port', c_uint16), + ('sin6_flowinfo', c_uint32), + ('sin6_addr', c_uint8 * 16), + ('sin6_scope_id', c_uint32) + ] + + def __str__(self): + try: + assert self.sin6_len >= sizeof(sockaddr_in6) + data = ''.join(map(chr, self.sin6_addr)) + return socket.inet_ntop(socket.AF_INET6, data) + except: + return '' + +class sockaddr_dl(Structure): + _fields_ = [ + ('sdl_len', c_uint8), + ('sdl_family', c_uint8), + ('sdl_index', c_short), + ('sdl_type', c_uint8), + ('sdl_nlen', c_uint8), + ('sdl_alen', c_uint8), + ('sdl_slen', c_uint8), + ('sdl_data', c_uint8 * 12) + ] + + def __str__(self): + assert self.sdl_len >= sizeof(sockaddr_dl) + addrdata = self.sdl_data[self.sdl_nlen:self.sdl_nlen+self.sdl_alen] + return ':'.join('%02x' % x for x in addrdata) + +class sockaddr_storage(Structure): + _fields_ = [ + ('sa_len', c_uint8), + ('sa_family', c_uint8), + ('sa_data', c_uint8 * 254) + ] + +class sockaddr(Union): + _anonymous_ = ('sa_storage', ) + _fields_ = [ + ('sa_storage', sockaddr_storage), + ('sa_sin', sockaddr_in), + ('sa_sin6', sockaddr_in6), + ('sa_sdl', sockaddr_dl), + ] + + def family(self): + return self.sa_storage.sa_family + + def __str__(self): + family = self.family() + if family == socket.AF_INET: + return str(self.sa_sin) + elif family == socket.AF_INET6: + return str(self.sa_sin6) + elif family == 18: # AF_LINK + return str(self.sa_sdl) + else: + print(family) + raise NotImplementedError("address family %d not supported" % family) + + +class ifaddrs(Structure): + pass + +ifaddrs._fields_ = [ + ('ifa_next', POINTER(ifaddrs)), + ('ifa_name', c_char_p), + ('ifa_flags', c_uint), + ('ifa_addr', POINTER(sockaddr)), + ('ifa_netmask', POINTER(sockaddr)), + ('ifa_dstaddr', POINTER(sockaddr)), + ('ifa_data', c_void_p) +] + +# Define constants for the most useful interface flags (from if.h). +IFF_UP = 0x0001 +IFF_BROADCAST = 0x0002 +IFF_LOOPBACK = 0x0008 +IFF_POINTTOPOINT = 0x0010 +IFF_RUNNING = 0x0040 +if sys.platform == 'darwin' or 'bsd' in sys.platform: + IFF_MULTICAST = 0x8000 +elif sys.platform == 'linux': + IFF_MULTICAST = 0x1000 + +# Load library implementing getifaddrs and freeifaddrs. +if sys.platform == 'darwin': + libc = cdll.LoadLibrary('libc.dylib') +else: + libc = cdll.LoadLibrary('libc.so') + +# Tell ctypes the argument and return types for the getifaddrs and +# freeifaddrs functions so it can do marshalling for us. +libc.getifaddrs.argtypes = [POINTER(POINTER(ifaddrs))] +libc.getifaddrs.restype = c_int +libc.freeifaddrs.argtypes = [POINTER(ifaddrs)] + + +def getifaddrs(): + """ + Get local interface addresses. + + Returns generator of tuples consisting of interface name, interface flags, + address family (e.g. socket.AF_INET, socket.AF_INET6), address, and netmask. + The tuple members can also be accessed via the names 'name', 'flags', + 'family', 'address', and 'netmask', respectively. + """ + # Get address information for each interface. + addrlist = POINTER(ifaddrs)() + if libc.getifaddrs(pointer(addrlist)) < 0: + raise OSError + + X = namedtuple('ifaddrs', 'name flags family address netmask') + + # Iterate through the address information. + ifaddr = addrlist + while ifaddr and ifaddr.contents: + # The following is a hack to workaround a bug in FreeBSD + # (PR kern/152036) and MacOSX wherein the netmask's sockaddr may be + # truncated. Specifically, AF_INET netmasks may have their sin_addr + # member truncated to the minimum number of bytes necessary to + # represent the netmask. For example, a sockaddr_in with the netmask + # 255.255.254.0 may be truncated to 7 bytes (rather than the normal + # 16) such that the sin_addr field only contains 0xff, 0xff, 0xfe. + # All bytes beyond sa_len bytes are assumed to be zero. Here we work + # around this truncation by copying the netmask's sockaddr into a + # zero-filled buffer. + if ifaddr.contents.ifa_netmask: + netmask = sockaddr() + memmove(byref(netmask), ifaddr.contents.ifa_netmask, + ifaddr.contents.ifa_netmask.contents.sa_len) + if netmask.sa_family == socket.AF_INET and netmask.sa_len < sizeof(sockaddr_in): + netmask.sa_len = sizeof(sockaddr_in) + else: + netmask = None + + try: + yield X(ifaddr.contents.ifa_name, + ifaddr.contents.ifa_flags, + ifaddr.contents.ifa_addr.contents.family(), + str(ifaddr.contents.ifa_addr.contents), + str(netmask) if netmask else None) + except NotImplementedError: + # Unsupported address family. + yield X(ifaddr.contents.ifa_name, + ifaddr.contents.ifa_flags, + None, + None, + None) + ifaddr = ifaddr.contents.ifa_next + + # When we are done with the address list, ask libc to free whatever memory + # it allocated for the list. + libc.freeifaddrs(addrlist) + +__all__ = ['getifaddrs'] + [n for n in dir() if n.startswith('IFF_')] \ No newline at end of file diff --git a/script.plexmod/lib/_included_packages/plexnet/netif/ipconfig.py b/script.plexmod/lib/_included_packages/plexnet/netif/ipconfig.py new file mode 100644 index 000000000..24804348c --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/netif/ipconfig.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from __future__ import print_function +import subprocess +import sys + +def parse(data=None): + kwa = {} + if sys.version_info[0] >= 3: + kwa = {"encoding": "latin-1"} + + data = data or subprocess.check_output('ipconfig /all',startupinfo=getStartupInfo(), **kwa) + dlist = [d.rstrip() for d in data.split('\n')] + mode = None + sections = [] + while dlist: + d = dlist.pop(0) + try: + if not d: + continue + elif not d.startswith(' '): + sections.append({'name':d.strip('.: ')}) + elif d.startswith(' '): + if d.endswith(':'): + k = d.strip(':. ') + mode = 'VALUE:' + k + sections[-1][k] = '' + elif ':' in d: + k,v = d.split(':',1) + k = k.strip(':. ') + mode = 'VALUE:' + k + v = v.replace('(Preferred)','') + sections[-1][k] = v.strip() + elif mode and mode.startswith('VALUE:'): + if not d.startswith(' '): + mode = None + dlist.insert(0,d) + continue + k = mode.split(':',1)[-1] + v = d.replace('(Preferred)','') + sections[-1][k] += ',' + v.strip() + except: + print(d) + raise + + return sections[1:] + +def getStartupInfo(): + if hasattr(subprocess,'STARTUPINFO'): #Windows + startupinfo = subprocess.STARTUPINFO() + try: + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #Suppress terminal window + except: + startupinfo.dwFlags |= 1 + return startupinfo + + return None \ No newline at end of file diff --git a/script.plexmod/lib/_included_packages/plexnet/netif/winpsif.py b/script.plexmod/lib/_included_packages/plexnet/netif/winpsif.py new file mode 100644 index 000000000..db13eca88 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/netif/winpsif.py @@ -0,0 +1,22 @@ +# coding=utf-8 + +import subprocess +import re +import sys + +PS_RE = re.compile(r'(?mu)^Description\s+: (?P[^\n]+)\nIPAddress\s+: {(?P[0-9.]+),' + r'.+\nIPSubnet\s+: {(?P[0-9.]+),.+\n') + + +def getInterfaces(): + """ + Use powershell to retrieve interfaces; parse using PS_RE, returning tuples of (name, ipaddress, subnet) + """ + kwa = {} + if sys.version_info[0] >= 3: + kwa = {"encoding": "windows-1252"} + + return PS_RE.findall(subprocess.check_output( + 'powershell "Get-WmiObject -Class Win32_NetworkAdapterConfiguration | ' + 'Select-Object Description, IPAddress, IPSubnet | Format-List"', + shell=True, **kwa)) diff --git a/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py b/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py new file mode 100644 index 000000000..6d043f24c --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py @@ -0,0 +1,215 @@ +# Most of this is ported from Roku code and much of it is currently unused +# TODO: Perhaps remove unnecessary code +from __future__ import absolute_import +import time + +from . import util +from six import moves +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import six.moves.urllib.parse +from . import plexrequest +from . import callback +from . import http + + +class ServerTimeline(util.AttributeDict): + def reset(self): + self.expires = time.time() + 15 + + def isExpired(self): + return time.time() > self.get('expires', 0) + + +class TimelineData(util.AttributeDict): + def __init__(self, timelineType, *args, **kwargs): + util.AttributeDict.__init__(self, *args, **kwargs) + self.type = timelineType + self.state = "stopped" + self.item = None + self.choice = None + self.playQueue = None + + self.controllable = util.AttributeDict() + self.controllableStr = None + + self.attrs = util.AttributeDict() + + # Set default controllable for all content. Other controllable aspects + # will be set based on the players content. + # + self.setControllable("playPause", True) + self.setControllable("stop", True) + + def setControllable(self, name, isControllable): + if isControllable: + self.controllable[name] = "" + else: + if name in self.controllable: + del self.controllable[name] + + self.controllableStr = None + + def updateControllableStr(self): + if not self.controllableStr: + self.controllableStr = "" + prependComma = False + + for name in self.controllable: + if prependComma: + self.controllableStr += ',' + else: + prependComma = True + self.controllableStr += name + + def toXmlAttributes(self, elem): + self.updateControllableStr() + elem.attrib["type"] = self.type + elem.attrib["start"] = self.state + elem.attrib["controllable"] = self.controllableStr + + if self.item: + if self.item.duration: + elem.attrib['duration'] = self.item.duration + if self.item.ratingKey: + elem.attrib['ratingKey'] = self.item.ratingKey + if self.item.key: + elem.attrib['key'] = self.item.key + if self.item.container.address: + elem.attrib['containerKey'] = self.item.container.address + + # Send the audio, video and subtitle choice if it's available + if self.choice: + for stream in ("audioStream", "videoStream", "subtitleStream"): + if self.choice.get(stream) and self.choice[stream].id: + elem.attrib[stream + "ID"] = self.choice[stream].id + + server = self.item.getServer() + if server: + elem.attrib["machineIdentifier"] = server.uuid + + if server.activeConnection: + parts = six.moves.urllib.parse.uslparse(server.activeConnection.address) + elem.attrib["protocol"] = parts.scheme + elem.attrib["address"] = parts.netloc.split(':', 1)[0] + if ':' in parts.netloc: + elem.attrib["port"] = parts.netloc.split(':', 1)[-1] + elif parts.scheme == 'https': + elem.attrib["port"] = '443' + else: + elem.attrib["port"] = '80' + + if self.playQueue: + elem.attrib["playQueueID"] = str(self.playQueue.id) + elem.attrib["playQueueItemID"] = str(self.playQueue.selectedId) + elem.attrib["playQueueVersion"] = str(self.playQueue.version) + + for key, val in self.attrs.items(): + elem.attrib[key] = val + + +class NowPlayingManager(object): + def __init__(self): + # Constants + self.NAVIGATION = "navigation" + self.FULLSCREEN_VIDEO = "fullScreenVideo" + self.FULLSCREEN_MUSIC = "fullScreenMusic" + self.FULLSCREEN_PHOTO = "fullScreenPhoto" + self.TIMELINE_TYPES = ["video", "music", "photo"] + + # Members + self.serverTimelines = util.AttributeDict() + self.subscribers = util.AttributeDict() + self.pollReplies = util.AttributeDict() + self.timelines = util.AttributeDict() + self.location = self.NAVIGATION + + self.textFieldName = None + self.textFieldContent = None + self.textFieldSecure = None + + # Initialization + for timelineType in self.TIMELINE_TYPES: + self.timelines[timelineType] = TimelineData(timelineType) + + def updatePlaybackState(self, timelineType, playerObject, state, time, playQueue=None, duration=0): + timeline = self.timelines[timelineType] + timeline.state = state + timeline.item = playerObject.item + timeline.choice = playerObject.choice + timeline.playQueue = playQueue + timeline.attrs["time"] = str(time) + timeline.duration = duration + + # self.sendTimelineToAll() + + self.sendTimelineToServer(timelineType, timeline, time) + + def sendTimelineToServer(self, timelineType, timeline, time): + if not hasattr(timeline.item, 'getServer') or not timeline.item.getServer(): + return + + serverTimeline = self.getServerTimeline(timelineType) + + # Only send timeline if it's the first, item changes, playstate changes or timer pops + itemsEqual = timeline.item and serverTimeline.item and timeline.item.ratingKey == serverTimeline.item.ratingKey + if itemsEqual and timeline.state == serverTimeline.state and not serverTimeline.isExpired(): + return + + serverTimeline.reset() + serverTimeline.item = timeline.item + serverTimeline.state = timeline.state + + # Ignore sending timelines for multi part media with no duration + obj = timeline.choice + if obj and obj.part and obj.part.duration.asInt() == 0 and obj.media.parts and len(obj.media.parts) > 1: + util.WARN_LOG("Timeline not supported: the current part doesn't have a valid duration") + return + + # It's possible with timers and in player seeking for the time to be greater than the + # duration, which causes a 400, so in that case we'll set the time to the duration. + duration = timeline.item.duration.asInt() or timeline.duration + if time > duration: + time = duration + + params = util.AttributeDict() + params["time"] = time + params["duration"] = duration + params["state"] = timeline.state + params["guid"] = timeline.item.guid + params["ratingKey"] = timeline.item.ratingKey + params["url"] = timeline.item.url + params["key"] = timeline.item.key + params["containerKey"] = timeline.item.container.address + if timeline.playQueue: + params["playQueueItemID"] = timeline.playQueue.selectedId + + path = "/:/timeline" + for paramKey in params: + if params[paramKey]: + path = http.addUrlParam(path, paramKey + "=" + six.moves.urllib.parse.quote(str(params[paramKey]))) + + request = plexrequest.PlexRequest(timeline.item.getServer(), path) + + context = request.createRequestContext("timelineUpdate", callback.Callable(self.onTimelineResponse)) + context.playQueue = timeline.playQueue + util.APP.startRequest(request, context) + + def getServerTimeline(self, timelineType): + if not self.serverTimelines.get(timelineType): + serverTL = ServerTimeline() + serverTL.reset() + + self.serverTimelines[timelineType] = serverTL + + return self.serverTimelines[timelineType] + + def nowPlayingSetControllable(self, timelineType, name, isControllable): + self.timelines[timelineType].setControllable(name, isControllable) + + def onTimelineResponse(self, request, response, context): + context.request.server.trigger("np:timelineResponse", response=response) + + if not context.playQueue or not context.playQueue.refreshOnTimeline: + return + context.playQueue.refreshOnTimeline = False + context.playQueue.refresh(False) diff --git a/script.plexmod/lib/_included_packages/plexnet/photo.py b/script.plexmod/lib/_included_packages/plexnet/photo.py new file mode 100644 index 000000000..d3a66ca68 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/photo.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from . import media +from . import plexobjects +from . import plexmedia + + +class Photo(media.MediaItem): + TYPE = 'photo' + + def _setData(self, data): + self.art = plexobjects.PlexValue('') + media.MediaItem._setData(self, data) + + if self.isFullObject(): + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, + initpath=self.initpath, server=self.server, media=self) + + def analyze(self): + """ The primary purpose of media analysis is to gather information about that media + item. All of the media you add to a Library has properties that are useful to + know–whether it's a video file, a music track, or one of your photos. + """ + self.server.query('/%s/analyze' % self.key) + + def markWatched(self): + path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey + self.server.query(path) + self.reload() + + def markUnwatched(self): + path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey + self.server.query(path) + self.reload() + + def play(self, client): + client.playMedia(self) + + def refresh(self): + self.server.query('%s/refresh' % self.key, method=self.server.session.put) + + def isPhotoOrDirectoryItem(self): + return True + + +class PhotoDirectory(media.MediaItem): + TYPE = 'photodirectory' + + def all(self): + path = self.key + return plexobjects.listItems(self.server, path) + + def isPhotoOrDirectoryItem(self): + return True + + +@plexobjects.registerLibFactory('photo') +@plexobjects.registerLibFactory('image') +def PhotoFactory(data, initpath=None, server=None, container=None): + if data.tag == 'Photo': + return Photo(data, initpath=initpath, server=server, container=container) + else: + return PhotoDirectory(data, initpath=initpath, server=server, container=container) diff --git a/script.plexmod/lib/_included_packages/plexnet/playlist.py b/script.plexmod/lib/_included_packages/plexnet/playlist.py new file mode 100644 index 000000000..b06358dd7 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/playlist.py @@ -0,0 +1,183 @@ +from __future__ import absolute_import +import random + +from . import plexobjects +from . import signalsmixin +from six.moves import range + + +class BasePlaylist(plexobjects.PlexObject, signalsmixin.SignalsMixin): + TYPE = 'baseplaylist' + + isRemote = False + + def __init__(self, *args, **kwargs): + plexobjects.PlexObject.__init__(self, *args, **kwargs) + signalsmixin.SignalsMixin.__init__(self) + self._items = [] + self._shuffle = None + self.pos = 0 + self.startShuffled = False + self.isRepeat = False + self.isRepeatOne = False + + def __getitem__(self, idx): + if self._shuffle: + return self._items[self._shuffle[idx]] + else: + return self._items[idx] + + def __iter__(self): + if self._shuffle: + for i in self._shuffle: + yield self._items[i] + else: + for i in self._items: + yield i + + def __len__(self): + return len(self._items) + + def items(self): + if self._shuffle: + return [i for i in self] + else: + return self._items + + def setRepeat(self, repeat, one=False): + if self.isRepeat == repeat and self.isRepeatOne == one: + return + + self.isRepeat = repeat + self.isRepeatOne = one + + def hasNext(self): + if len(self._items) < 2: + return False + if self.isRepeatOne: + return False + if self.isRepeat: + return True + return self.pos < len(self._items) - 1 + + def hasPrev(self): + if len(self._items) < 2: + return False + if self.isRepeatOne: + return False + if self.isRepeat: + return True + return self.pos > 0 + + def next(self): + if not self.hasNext(): + return False + + if self.isRepeatOne: + return True + + self.pos += 1 + if self.pos >= len(self._items): + self.pos = 0 + + return True + + __next__ = next + + def prev(self): + if not self.hasPrev(): + return False + + if self.isRepeatOne: + return True + + self.pos -= 1 + if self.pos < 0: + self.pos = len(self._items) - 1 + + return True + + def getPosFromItem(self, item): + if item not in self._items: + return -1 + return self._items.index(item) + + def setCurrent(self, pos): + if not isinstance(pos, int): + item = pos + pos = self.getPosFromItem(item) + self._items[pos] = item + + if pos < 0 or pos >= len(self._items): + return False + + self.pos = pos + return True + + def current(self): + return self[self.pos] + + def userCurrent(self): + for item in self._items: + if not item.isWatched or item.viewOffset.asInt(): + return item + else: + return self.current() + + def prevItem(self): + if self.pos < 1: + return None + return self[self.pos - 1] + + def shuffle(self, on=True, first=False): + if on and self._items: + self._shuffle = list(range(len(self._items))) + random.shuffle(self._shuffle) + if not first: + self.pos = self._shuffle.index(self.pos) + else: + if self._shuffle: + self.pos = self._shuffle[self.pos] + if not first: + self._shuffle = None + self.trigger('items.changed') + self.refresh() + + def setShuffle(self, shuffle=None): + if shuffle is None: + shuffle = not self.isShuffled + + self.shuffle(shuffle) + + @property + def isShuffled(self): + return bool(self._shuffle) + + def refresh(self, *args, **kwargs): + self.trigger('change') + + +class LocalPlaylist(BasePlaylist): + TYPE = 'localplaylist' + + def __init__(self, items, server, media_item=None): + BasePlaylist.__init__(self, None, server=server) + self._items = items + self._mediaItem = media_item + + def __getattr__(self, name): + if not self._mediaItem: + return BasePlaylist.__getattr__(self, name) + return getattr(self._mediaItem, name) + + def get(self, name, default=''): + if not self._mediaItem: + return BasePlaylist.get(self, name, default) + + return self._mediaItem.get(name, default) + + @property + def defaultArt(self): + if not self._mediaItem: + return super(LocalPlaylist, self).defaultArt + return self._mediaItem.defaultArt diff --git a/script.plexmod/lib/_included_packages/plexnet/playqueue.py b/script.plexmod/lib/_included_packages/plexnet/playqueue.py new file mode 100644 index 000000000..a7adc3b0c --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/playqueue.py @@ -0,0 +1,786 @@ +from __future__ import absolute_import +import re +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import time + +from . import plexapp +from . import plexrequest +from . import callback +from . import plexobjects +from . import util +from . import signalsmixin +from six.moves import range + + +class AudioUsage(object): + def __init__(self, skipsPerHour, playQueueId): + self.HOUR = 3600 + self.skipsPerHour = skipsPerHour + self.playQueueId = playQueueId + self.skips = [] + + def allowSkip(self): + if self.skipsPerHour < 0: + return True + self.updateSkips() + return len(self.skips) < self.skipsPerHour + + def updateSkips(self, reset=False): + if reset or len(self.skips) == 0: + if reset: + self.skips = [] + return + + # Remove old skips if applicable + epoch = util.now() + if self.skips[0] + self.HOUR < epoch: + newSkips = [] + for skip in self.skips: + if skip + self.HOUR > epoch: + newSkips.append(skip) + self.skips = newSkips + self.log("updated skips") + + def registerSkip(self): + self.skips.append(util.now()) + self.updateSkips() + self.log("registered skip") + + def allowSkipMessage(self): + if self.skipsPerHour < 0 or self.allowSkip(): + return None + return "You can skip {0} songs an hour per mix.".format(self.skipsPerHour) + + def log(self, prefix): + util.DEBUG_LOG("AudioUsage {0}: total skips={1}, allowed skips={2}".format(prefix, len(self.skips), self.skipsPerHour)) + + +class UsageFactory(object): + def __init__(self, play_queue): + self.playQueue = play_queue + self.type = play_queue.type + self.usage = play_queue.usage + + @classmethod + def createUsage(cls, playQueue): + obj = cls(playQueue) + + if obj.type: + if obj.type == "audio": + return obj.createAudioUsage() + + util.DEBUG_LOG("Don't know how to usage for " + str(obj.type)) + return None + + def createAudioUsage(self): + skips = self.playQueue.container.stationSkipsPerHour.asInt(-1) + if skips == -1: + return None + + # Create new usage if invalid, or if we start a new PQ, otherwise + # we'll return the existing usage for the PQ. + if not self.usage or self.usage.playQueueId != self.playQueue.id: + self.usage = AudioUsage(skips, self.playQueue.id) + + return self.usage + + +class PlayOptions(util.AttributeDict): + def __init__(self, *args, **kwargs): + util.AttributeDict.__init__(self, *args, **kwargs) + # At the moment, this is really just a glorified struct. But the + # expected fields include key, shuffle, extraPrefixCount, + # and unwatched. We may give this more definition over time. + + # These aren't widely used yet, but half inspired by a PMS discussion... + self.CONTEXT_AUTO = 0 + self.CONTEXT_SELF = 1 + self.CONTEXT_PARENT = 2 + self.CONTEXT_CONTAINER = 3 + + self.context = self.CONTEXT_AUTO + + +def createLocalPlayQueue(item, children, contentType, options): + pass + + +class PlayQueueFactory(object): + def getContentType(self, item): + if item.isMusicOrDirectoryItem(): + return "audio" + elif item.isVideoOrDirectoryItem(): + return "video" + elif item.isPhotoOrDirectoryItem(): + return "photo" + + return None + + def canCreateRemotePlayQueue(self): + if self.item.getServer().isSecondary(): + reason = "Server is secondary" + elif not (self.item.isLibraryItem() or self.item.isGracenoteCollection() or self.item.isLibraryPQ or self.item.isCollection): + reason = "Item is not a library item or gracenote collection" + else: + return True + + util.DEBUG_LOG("Requires local play queue: " + reason) + return False + + def itemRequiresRemotePlayQueue(self): + # TODO(rob): handle entire section? (if we create PQ's of sections) + # return item instanceof PlexSection || item.type == PlexObject.Type.artist; + return self.item.type == "artist" + + +def createPlayQueueForItem(item, children=None, options=None, args=None): + obj = PlayQueueFactory() + + contentType = obj.getContentType(item) + if not contentType: + # TODO(schuyler): We may need to try harder, but I'm not sure yet. For + # example, what if we're shuffling an entire library? + # + # No reason to crash here. We can safely return None and move on. + # We'll stop if we're in dev mode to catch and debug. + # + util.DEBUG_LOG("Don't know how to create play queue for item " + repr(item)) + return None + + obj.item = item + + options = PlayOptions(options or {}) + + if obj.canCreateRemotePlayQueue(): + return createRemotePlayQueue(item, contentType, options, args) + else: + if obj.itemRequiresRemotePlayQueue(): + util.DEBUG_LOG("Can't create remote PQs and item does not support local PQs") + return None + else: + return createLocalPlayQueue(item, children, contentType, options) + + +class PlayQueue(signalsmixin.SignalsMixin): + TYPE = 'playqueue' + + isRemote = True + + def __init__(self, server, contentType, options=None): + signalsmixin.SignalsMixin.__init__(self) + self.id = None + self.selectedId = None + self.version = -1 + self.isShuffled = False + self.isRepeat = False + self.isRepeatOne = False + self.isLocalPlayQueue = False + self.isMixed = None + self.totalSize = 0 + self.windowSize = 0 + self.forcedWindow = False + self.container = None + + # Forced limitations + self.allowShuffle = False + self.allowSeek = True + self.allowRepeat = False + self.allowSkipPrev = False + self.allowSkipNext = False + self.allowAddToQueue = False + + self.refreshOnTimeline = False + + self.server = server + self.type = contentType + self._items = [] + self.options = options or util.AttributeDict() + + self.usage = None + + self.refreshTimer = None + + self.canceled = False + self.responded = False + self.initialized = False + + self.composite = plexobjects.PlexValue('', parent=self) + + # Add a few default options for specific PQ types + if self.type == "audio": + self.options.includeRelated = True + elif self.type == "photo": + self.setRepeat(True) + + def get(self, name): + return getattr(self, name, plexobjects.PlexValue('', parent=self)) + + @property + def defaultArt(self): + return self.current().defaultArt + + def waitForInitialization(self): + start = time.time() + timeout = util.TIMEOUT + util.DEBUG_LOG('Waiting for playQueue to initialize...') + while not self.canceled and not self.initialized: + if not self.responded and time.time() - start > timeout: + util.DEBUG_LOG('PlayQueue timed out wating for initialization') + return self.initialized + time.sleep(0.1) + + if self.initialized: + util.DEBUG_LOG('PlayQueue initialized in {0:.2f} secs: {1}'.format(time.time() - start, self)) + else: + util.DEBUG_LOG('PlayQueue failed to initialize') + + return self.initialized + + def onRefreshTimer(self): + self.refreshTimer = None + self.refresh(True, False) + + def refresh(self, force=True, delay=False, wait=False): + # Ignore refreshing local PQs + if self.isLocal(): + return + + if wait: + self.responded = False + self.initialized = False + # We refresh our play queue if the caller insists or if we only have a + # portion of our play queue loaded. In particular, this means that we don't + # refresh the play queue if we're asked to refresh because a new track is + # being played but we have the entire album loaded already. + + if force or self.isWindowed(): + if delay: + # We occasionally want to refresh the PQ in response to moving to a + # new item and starting playback, but if we refresh immediately: + # we probably end up refreshing before PMS realizes we've moved on. + # There's no great solution, but delaying our refresh by just a few + # seconds makes us much more likely to get an accurate window (and + # accurate selected IDs) from PMS. + + if not self.refreshTimer: + self.refreshTimer = plexapp.createTimer(5000, self.onRefreshTimer) + util.APP.addTimer(self.refreshTimer) + else: + request = plexrequest.PlexRequest(self.server, "/playQueues/" + str(self.id)) + self.addRequestOptions(request) + context = request.createRequestContext("refresh", callback.Callable(self.onResponse)) + util.APP.startRequest(request, context) + + if wait: + return self.waitForInitialization() + + def shuffle(self, shuffle=True): + self.setShuffle(shuffle) + + def setShuffle(self, shuffle=None): + if shuffle is None: + shuffle = not self.isShuffled + + if self.isShuffled == shuffle: + return + + if shuffle: + command = "/shuffle" + else: + command = "/unshuffle" + + # Don't change self.isShuffled, it'll be set in OnResponse if all goes well + + request = plexrequest.PlexRequest(self.server, "/playQueues/" + str(self.id) + command, "PUT") + self.addRequestOptions(request) + context = request.createRequestContext("shuffle", callback.Callable(self.onResponse)) + util.APP.startRequest(request, context) + + def setRepeat(self, repeat, one=False): + if self.isRepeat == repeat and self.isRepeatOne == one: + return + + self.options.repeat = repeat + self.isRepeat = repeat + self.isRepeatOne = one + + def moveItemUp(self, item): + for index in range(1, len(self._items)): + if self._items[index].get("playQueueItemID") == item.get("playQueueItemID"): + if index > 1: + after = self._items[index - 2] + else: + after = None + + self.swapItem(index, -1) + self.moveItem(item, after) + return True + + return False + + def moveItemDown(self, item): + for index in range(len(self._items) - 1): + if self._items[index].get("playQueueItemID") == item.get("playQueueItemID"): + after = self._items[index + 1] + self.swapItem(index) + self.moveItem(item, after) + return True + + return False + + def moveItem(self, item, after): + if after: + query = "?after=" + after.get("playQueueItemID", "-1") + else: + query = "" + + request = plexrequest.PlexRequest(self.server, "/playQueues/" + str(self.id) + "/items/" + item.get("playQueueItemID", "-1") + "/move" + query, "PUT") + self.addRequestOptions(request) + context = request.createRequestContext("move", callback.Callable(self.onResponse)) + util.APP.startRequest(request, context) + + def swapItem(self, index, delta=1): + before = self._items[index] + after = self._items[index + delta] + + self._items[index] = after + self._items[index + delta] = before + + def removeItem(self, item): + request = plexrequest.PlexRequest(self.server, "/playQueues/" + str(self.id) + "/items/" + item.get("playQueueItemID", "-1"), "DELETE") + self.addRequestOptions(request) + context = request.createRequestContext("delete", callback.Callable(self.onResponse)) + util.APP.startRequest(request, context) + + def addItem(self, item, addNext=False, excludeSeedItem=False): + request = plexrequest.PlexRequest(self.server, "/playQueues/" + str(self.id), "PUT") + request.addParam("uri", item.getItemUri()) + request.addParam("next", addNext and "1" or "0") + request.addParam("excludeSeedItem", excludeSeedItem and "1" or "0") + self.addRequestOptions(request) + context = request.createRequestContext("add", callback.Callable(self.onResponse)) + util.APP.startRequest(request, context) + + def onResponse(self, request, response, context): + # Close any loading modal regardless of response status + # Application().closeLoadingModal() + util.DEBUG_LOG('playQueue: Received response') + self.responded = True + if response.parseResponse(): + util.DEBUG_LOG('playQueue: {0} items'.format(len(response.items))) + self.container = response.container + # Handle an empty PQ if we have specified an pqEmptyCallable + if self.options and self.options.pqEmptyCallable: + callable = self.options.pqEmptyCallable + del self.options["pqEmptyCallable"] + if len(response.items) == 0: + callable.call() + return + + self.id = response.container.playQueueID.asInt() + self.isShuffled = response.container.playQueueShuffled.asBool() + self.totalSize = response.container.playQueueTotalCount.asInt() + self.windowSize = len(response.items) + self.version = response.container.playQueueVersion.asInt() + + itemsChanged = False + if len(response.items) == len(self._items): + for i in range(len(self._items)): + if self._items[i] != response.items[i]: + itemsChanged = True + break + else: + itemsChanged = True + + if itemsChanged: + self._items = response.items + + # Process any forced limitations + self.allowSeek = response.container.allowSeek.asBool() + self.allowShuffle = ( + self.totalSize > 1 and response.container.allowShuffle.asBool() and not response.container.playQueueLastAddedItemID + ) + self.allowRepeat = response.container.allowRepeat.asBool() + self.allowSkipPrev = self.totalSize > 1 and response.container.allowSkipPrevious != "0" + self.allowSkipNext = self.totalSize > 1 and response.container.allowSkipNext != "0" + + # Figure out the selected track index and offset. PMS tries to make some + # of this easy, but it might not realize that we've advanced to a new + # track, so we can't blindly trust it. On the other hand, it's possible + # that PMS completely changed the PQ item IDs (e.g. upon shuffling), so + # we might need to use its values. We iterate through the items and try + # to find the item that we believe is selected, only settling for what + # PMS says if we fail. + + playQueueOffset = None + selectedId = None + pmsSelectedId = response.container.playQueueSelectedItemID.asInt() + self.deriveIsMixed() + + # lastItem = None # Not used + for index in range(len(self._items)): + item = self._items[index] + + if not playQueueOffset and item.playQueueItemID.asInt() == pmsSelectedId: + playQueueOffset = response.container.playQueueSelectedItemOffset.asInt() - index + 1 + + # Update the index of everything we've already past, and handle + # wrapping indexes (repeat). + for i in range(index): + pqIndex = playQueueOffset + i + if pqIndex < 1: + pqIndex = pqIndex + self.totalSize + + self._items[i].playQueueIndex = plexobjects.PlexValue(str(pqIndex), parent=self._items[i]) + + if playQueueOffset: + pqIndex = playQueueOffset + index + if pqIndex > self.totalSize: + pqIndex = pqIndex - self.totalSize + + item.playQueueIndex = plexobjects.PlexValue(str(pqIndex), parent=item) + + # If we found the item that we believe is selected: we should + # continue to treat it as selected. + # TODO(schuyler): Should we be checking the metadata ID (rating key) + # instead? I don't think it matters in practice, but it may be + # more correct. + + if not selectedId and item.playQueueItemID.asInt() == self.selectedId: + selectedId = self.selectedId + + if not selectedId: + self.selectedId = pmsSelectedId + + # TODO(schuyler): Set repeat as soon as PMS starts returning it + + # Fix up the container for all our items + response.container.address = "/playQueues/" + str(self.id) + + # Create usage limitations + self.usage = UsageFactory.createUsage(self) + + self.initialized = True + self.trigger("change") + + if itemsChanged: + self.trigger("items.changed") + + def isWindowed(self): + return (not self.isLocal() and (self.totalSize > self.windowSize or self.forcedWindow)) + + def hasNext(self): + if self.isRepeatOne: + return True + + if not self.allowSkipNext and -1 < list(self.items()).index(self.current()) < (len(list(self.items())) - 1): # TODO: Was 'or' - did change cause issues? + return self.isRepeat and not self.isWindowed() + + return True + + def hasPrev(self): + # return self.allowSkipPrev or self.items().index(self.current()) > 0 + return list(self.items()).index(self.current()) > 0 + + def next(self): + if self.isRepeatOne: + return self.current() + + item = self.getNext() + if not item: + return None + + self.selectedId = item.playQueueItemID.asInt() + return item + + __next__ = next + + def prev(self): + if self.isRepeatOne: + return self.current() + + item = self.getPrev() + if not item: + return None + + self.selectedId = item.playQueueItemID.asInt() + return item + + def getPrev(self): + if not self.hasPrev(): + return None + + pos = list(self.items()).index(self.current()) - 1 + return list(self.items())[pos] + + def getNext(self): + if not self.hasNext(): + return None + + pos = list(self.items()).index(self.current()) + 1 + if pos >= len(self.items()): + if not self.isRepeat or self.isWindowed(): + return None + pos = 0 + + return list(self.items())[pos] + + + def setCurrent(self, pos): + if pos < 0 or pos >= len(list(self.items())): + return False + + item = list(self.items())[pos] + self.selectedId = item.playQueueItemID.asInt() + return item + + def setCurrentItem(self, item): + self.selectedId = item.playQueueItemID.asInt() + + def __eq__(self, other): + if not other: + return False + if self.__class__ != other.__class__: + return False + return self.id == other.id and self.type == other.type + + def __ne__(self, other): + return not self.__eq__(other) + + def addRequestOptions(self, request): + boolOpts = ["repeat", "includeRelated"] + for opt in boolOpts: + if self.options.get(opt): + request.addParam(opt, "1") + + intOpts = ["extrasPrefixCount"] + for opt in intOpts: + if self.options.get(opt): + request.addParam(opt, str(self.options.get(opt))) + + includeChapters = self.options.get('includeChapters') is not None and self.options.includeChapters or 1 + request.addParam("includeChapters", str(includeChapters)) + + def __repr__(self): + return ( + str(self.__class__.__name__) + " " + + str(self.type) + " windowSize=" + + str(self.windowSize) + " totalSize=" + + str(self.totalSize) + " selectedId=" + + str(self.selectedId) + " shuffled=" + + str(self.isShuffled) + " repeat=" + + str(self.isRepeat) + " mixed=" + + str(self.isMixed) + " allowShuffle=" + + str(self.allowShuffle) + " version=" + + str(self.version) + " id=" + str(self.id) + ) + + def isLocal(self): + return self.isLocalPlayQueue + + def deriveIsMixed(self): + if self.isMixed is None: + self.isMixed = False + + lastItem = None + for item in self._items: + if not self.isMixed: + if not item.get("parentKey"): + self.isMixed = True + else: + self.isMixed = lastItem and item.get("parentKey") != lastItem.get("parentKey") + + lastItem = item + + def items(self): + return self._items + + def current(self): + for item in self.items(): + if item.playQueueItemID.asInt() == self.selectedId: + return item + + return None + + def prevItem(self): + last = None + for item in self.items(): + if item.playQueueItemID.asInt() == self.selectedId: + return last + last = item + + return None + + +def createRemotePlayQueue(item, contentType, options, args): + util.DEBUG_LOG('Creating remote playQueue request...') + obj = PlayQueue(item.getServer(), contentType, options) + + # The item's URI is made up of the library section UUID, a descriptor of + # the item type (item or directory), and the item's path, URL-encoded. + + uri = "library://" + item.getLibrarySectionUuid() + "/" + itemType = item.isDirectory() and "directory" or "item" + path = None + + if not options.key: + # if item.onDeck and len(item.onDeck) > 0: + # options.key = item.onDeck[0].getAbsolutePath("key") + # el + if not item.isDirectory(): + options.key = item.get("key") + + # If we're asked to play unwatched, ignore the option unless we are unwatched. + options.unwatched = options.unwatched and item.isUnwatched() + + # TODO(schuyler): Until we build postplay, we're not allowed to queue containers for episodes. + if item.type == "episode": + options.context = options.CONTEXT_SELF + elif item.type == "movie": + if not options.extrasPrefixCount and not options.resume: + options.extrasPrefixCount = util.INTERFACE.getPreference("cinema_trailers", 0) + + # How exactly to construct the item URI depends on the metadata type, though + # whenever possible we simply use /library/metadata/:id. + + if item.isLibraryItem() and not item.isLibraryPQ: + path = "/library/metadata/" + item.ratingKey + else: + path = item.getAbsolutePath("key") + + if options.context == options.CONTEXT_SELF: + # If the context is specifically for just this item,: just use the + # item's key and get out. + pass + elif item.type == "playlist": + path = None + uri = item.get("ratingKey") + options.isPlaylist = True + elif item.type == "track": + # TODO(rob): Is there ever a time the container address is wrong? If we + # expect to play a single track,: use options.CONTEXT_SELF. + path = item.container.address or "/library/metadata/" + item.get("parentRatingKey", "") + itemType = "directory" + elif item.isPhotoOrDirectoryItem(): + if item.type == "photoalbum" or item.parentKey: + path = item.getParentPath(item.type == "photoalbum" and "key" or "parentKey") + itemType = "item" + elif item.isDirectory(): + path = item.getAbsolutePath("key") + else: + path = item.container.address + itemType = "directory" + options.key = item.getAbsolutePath("key") + + elif item.type == "episode": + path = "/library/metadata/" + item.get("grandparentRatingKey", "") + itemType = "directory" + options.key = item.getAbsolutePath("key") + # elif item.type == "show": + # path = "/library/metadata/" + item.get("ratingKey", "") + + if path: + if args: + path += util.joinArgs(args) + + util.DEBUG_LOG("playQueue path: " + str(path)) + + if "/search" not in path: + # Convert a few params to the PQ spec + convert = { + 'type': "sourceType", + 'unwatchedLeaves': "unwatched" + } + + for key in convert: + regex = re.compile("(?i)([?&])" + key + "=") + path = regex.sub("\1" + convert[key] + "=", path) + + util.DEBUG_LOG("playQueue path: " + str(path)) + uri = uri + itemType + "/" + six.moves.urllib.parse.quote_plus(path) + + util.DEBUG_LOG("playQueue uri: " + str(uri)) + + # Create the PQ request + request = plexrequest.PlexRequest(obj.server, "/playQueues") + + request.addParam(not options.isPlaylist and "uri" or "playlistID", uri) + request.addParam("type", contentType) + # request.addParam('X-Plex-Client-Identifier', util.INTERFACE.getGlobal('clientIdentifier')) + + # Add options we pass once during PQ creation + if options.shuffle: + request.addParam("shuffle", "1") + options.key = None + else: + request.addParam("shuffle", "0") + + if options.key: + request.addParam("key", options.key) + + # Add options we pass every time querying PQs + obj.addRequestOptions(request) + + util.DEBUG_LOG('Initial playQueue request started...') + context = request.createRequestContext("create", callback.Callable(obj.onResponse)) + util.APP.startRequest(request, context, body='') + + return obj + + +def createPlayQueueForId(id, server=None, contentType=None): + obj = PlayQueue(server, contentType) + obj.id = id + + request = plexrequest.PlexRequest(server, "/playQueues/" + str(id)) + request.addParam("own", "1") + obj.addRequestOptions(request) + context = request.createRequestContext("own", callback.Callable(obj.onResponse)) + util.APP.startRequest(request, context) + + return obj + + +class AudioPlayer(): + pass + + +class VideoPlayer(): + pass + + +class PhotoPlayer(): + pass + + +def addItemToPlayQueue(item, addNext=False): + # See if we have an active play queue for this self.dia type or if we need to + # create one. + + if item.isMusicOrDirectoryItem(): + player = AudioPlayer() + elif item.isVideoOrDirectoryItem(): + player = VideoPlayer() + elif item.isPhotoOrDirectoryItem(): + player = PhotoPlayer() + else: + player = None + + if not player: + util.ERROR_LOG("Don't know how to add item to play queue: " + str(item)) + return None + elif not player.allowAddToQueue(): + util.DEBUG_LOG("Not allowed to add to this player") + return None + + if player.playQueue: + playQueue = player.playQueue + playQueue.addItem(item, addNext) + else: + options = PlayOptions() + options.context = options.CONTEXT_SELF + playQueue = createPlayQueueForItem(item, None, options) + if playQueue: + player.setPlayQueue(playQueue, False) + + return playQueue diff --git a/script.plexmod/lib/_included_packages/plexnet/plexapp.py b/script.plexmod/lib/_included_packages/plexnet/plexapp.py new file mode 100644 index 000000000..cc7d087b1 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexapp.py @@ -0,0 +1,403 @@ +from __future__ import print_function, absolute_import +import threading +import platform +import uuid +import sys + +from . import callback +from . import signalsmixin +from . import simpleobjects +from . import util +import six + +Res = simpleobjects.Res +SERVERMANAGER = None +ACCOUNT = None +MANAGER = None + +PLATFORM = util.X_PLEX_DEVICE + + +def init(): + global MANAGER, SERVERMANAGER, ACCOUNT + from . import myplexaccount + ACCOUNT = myplexaccount.ACCOUNT + ACCOUNT.init() + from . import plexservermanager + SERVERMANAGER = plexservermanager.MANAGER + from . import myplexmanager + util.MANAGER = MANAGER = myplexmanager.MANAGER + ACCOUNT.verifyAccount() + + +class App(signalsmixin.SignalsMixin): + def __init__(self): + signalsmixin.SignalsMixin.__init__(self) + self.pendingRequests = {} + self.initializers = {} + self.timers = [] + from . import nowplayingmanager + self.nowplayingmanager = nowplayingmanager.NowPlayingManager() + + def addTimer(self, timer): + self.timers.append(timer) + + def startRequest(self, request, context, body=None, contentType=None): + context.request = request + + started = request.startAsync(body=body, contentType=contentType, context=context) + + if started: + requestID = context.request.getIdentity() + self.pendingRequests[requestID] = context + elif context.callback: + context.callback(None, context) + + return started + + def onRequestTimeout(self, context): + requestID = context.request.getIdentity() + + if requestID not in self.pendingRequests: + return + + context.request.cancel() + + util.WARN_LOG("Request to {0} timed out after {1} sec".format(util.cleanToken(context.request.url), context.timeout)) + + if context.callback: + context.callback(None, context) + + def delRequest(self, request): + requestID = request.getIdentity() + if requestID not in self.pendingRequests: + return + + del self.pendingRequests[requestID] + + def addInitializer(self, name): + self.initializers[name] = True + + def clearInitializer(self, name): + if name in self.initializers: + del self.initializers[name] + if self.isInitialized(): + self.onInitialized() + + def isInitialized(self): + return not self.initializers + + def onInitialized(self): + # Wire up a few of our own listeners + # PlexServerManager() + # self.on("change:user", callback.Callable(self.onAccountChange)) + + self.trigger('init') + + def cancelAllTimers(self): + for timer in self.timers: + timer.cancel() + + def preShutdown(self): + from . import http + http.HttpRequest._cancel = True + if self.pendingRequests: + util.DEBUG_LOG('Closing down {0} App() requests...'.format(len(self.pendingRequests))) + for k in list(self.pendingRequests.keys()): + p = self.pendingRequests.get(k) + if p: + p.request.cancel() + + if self.timers: + util.DEBUG_LOG('Canceling App() timers...') + self.cancelAllTimers() + + if SERVERMANAGER.selectedServer: + util.DEBUG_LOG('Closing server...') + SERVERMANAGER.selectedServer.close() + + def shutdown(self): + if self.timers: + util.DEBUG_LOG('Waiting for {0} App() timers: Started'.format(len(self.timers))) + + self.cancelAllTimers() + + for timer in self.timers: + timer.join() + + util.DEBUG_LOG('Waiting for App() timers: Finished') + + +class DeviceInfo(object): + def getCaptionsOption(self, key): + return None + + +class AppInterface(object): + QUALITY_LOCAL = 0 + QUALITY_REMOTE = 1 + QUALITY_ONLINE = 2 + + _globals = {} + + def __init__(self): + self.setQualities() + + def setQualities(self): + # Calculate the max quality based on 4k support + if self._globals.get("supports4k"): + maxQuality = simpleobjects.AttributeDict({ + 'height': 2160, + 'maxHeight': 2160, + 'origHeight': 1080 + }) + maxResolution = self._globals.get("Is4k") and "4k" or "1080p" + else: + maxQuality = simpleobjects.AttributeDict({ + 'height': 1080, + 'maxHeight': 1088 + }) + maxResolution = "1080p" + + self._globals['qualities'] = [ + simpleobjects.AttributeDict({'title': "Original", 'index': 13, 'maxBitrate': 1000000}), + simpleobjects.AttributeDict({'title': "20 Mbps " + maxResolution, 'index': 12, 'maxBitrate': 20000}), + simpleobjects.AttributeDict({'title': "12 Mbps " + maxResolution, 'index': 11, 'maxBitrate': 12000}), + simpleobjects.AttributeDict({'title': "10 Mbps " + maxResolution, 'index': 10, 'maxBitrate': 10000}), + simpleobjects.AttributeDict({'title': "8 Mbps " + maxResolution, 'index': 9, 'maxBitrate': 8000}), + simpleobjects.AttributeDict({'title': "4 Mbps 720p", 'index': 8, 'maxBitrate': 4000, 'maxHeight': 720}), + simpleobjects.AttributeDict({'title': "3 Mbps 720p", 'index': 7, 'maxBitrate': 3000, 'maxHeight': 720}), + simpleobjects.AttributeDict({'title': "2 Mbps 720p", 'index': 6, 'maxBitrate': 2000, 'maxHeight': 720}), + simpleobjects.AttributeDict({'title': "1.5 Mbps 480p", 'index': 5, 'maxBitrate': 1500, 'maxHeight': 480}), + simpleobjects.AttributeDict({'title': "720 Kbps", 'index': 4, 'maxBitrate': 720, 'maxHeight': 360}), + simpleobjects.AttributeDict({'title': "320 Kbps", 'index': 3, 'maxBitrate': 320, 'maxHeight': 360}), + maxQuality + ] + + for quality in self._globals['qualities']: + if quality.index is not None and quality.index >= 9: + quality.update(maxQuality) + + def getPreference(self, pref, default=None): + raise NotImplementedError + + def setPreference(self, pref, value): + raise NotImplementedError + + def clearRegistry(self, reg, sec=None): + raise NotImplementedError + + def getRegistry(self, reg, default=None, sec=None): + raise NotImplementedError + + def setRegistry(self, reg, value, sec=None): + raise NotImplementedError + + def getGlobal(self, glbl, default=None): + raise NotImplementedError + + def getCapabilities(self): + raise NotImplementedError + + def LOG(self, msg): + raise NotImplementedError + + def DEBUG_LOG(self, msg): + self.LOG(msg) + + def WARN_LOG(self, msg): + self.LOG(msg) + + def ERROR_LOG(self, msg): + self.LOG(msg) + + def ERROR(self, msg=None, err=None): + self.LOG(msg) + + def FATAL(self, msg=None): + self.ERROR_LOG('FATAL: {0}'.format(msg)) + + def supportsAudioStream(self, codec, channels): + return False + + def supportsSurroundSound(self): + return False + + def getMaxResolution(self, quality_type, allow4k=False): + return 480 + + def getQualityIndex(self, qualityType): + if qualityType == self.QUALITY_LOCAL: + return self.getPreference("local_quality", 13) + elif qualityType == self.QUALITY_ONLINE: + return self.getPreference("online_quality", 13) + else: + return self.getPreference("remote_quality", 13) + + def settingsGetMaxResolution(self, qualityType, allow4k): + qualityIndex = self.getQualityIndex(qualityType) + + if qualityIndex >= 9: + return allow4k and 2160 or 1088 + elif qualityIndex >= 6: + return 720 + elif qualityIndex >= 5: + return 480 + else: + return 360 + + def getMaxBitrate(self, qualityType): + qualityIndex = self.getQualityIndex(qualityType) + + qualities = self.getGlobal("qualities", []) + for quality in qualities: + if quality.index == qualityIndex: + return util.validInt(quality.maxBitrate) + + return 0 + + +class PlayerSettingsInterface(object): + def __init__(self): + self.prefOverrides = {} + + def __getattr__(self, name): + return getattr(util.INTERFACE, name) + + def setPrefOverride(self, key, val): + self.prefOverrides[key] = val + + def getPrefOverride(self, key, default=None): + return self.prefOverrides.get(key, default) + + def getQualityIndex(self, qualityType): + if qualityType == util.INTERFACE.QUALITY_LOCAL: + return self.getPreference("local_quality", 13) + elif qualityType == util.INTERFACE.QUALITY_ONLINE: + return self.getPreference("online_quality", 13) + else: + return self.getPreference("remote_quality", 13) + + def getPreference(self, key, default=None): + if key in self.prefOverrides: + return self.prefOverrides[key] + else: + return util.INTERFACE.getPreference(key, default) + + def getMaxResolution(self, quality_type, allow4k=False): + qualityIndex = self.getQualityIndex(quality_type) + + if qualityIndex >= 9: + return allow4k and 2160 or 1088 + elif qualityIndex >= 6: + return 720 + elif qualityIndex >= 5: + return 480 + else: + return 360 + + def getMaxBitrate(self, qualityType): + qualityIndex = self.getQualityIndex(qualityType) + + qualities = self.getGlobal("qualities", []) + for quality in qualities: + if quality.index == qualityIndex: + return util.validInt(quality.maxBitrate) + + return 0 + + +class DumbInterface(AppInterface): + _prefs = {} + _regs = { + None: {} + } + _globals = { + 'platform': platform.uname()[0], + 'appVersionStr': '0.0.0a1', + 'clientIdentifier': str(hex(uuid.getnode())), + 'platformVersion': platform.uname()[2], + 'product': 'PlexNet.API', + 'provides': 'player', + 'device': platform.uname()[0], + 'model': 'Unknown', + 'friendlyName': 'PlexNet.API', + 'deviceInfo': DeviceInfo() + } + + def getPreference(self, pref, default=None): + return self._prefs.get(pref, default) + + def setPreference(self, pref, value): + self._prefs[pref] = value + + def getRegistry(self, reg, default=None, sec=None): + section = self._regs.get(sec) + if section: + return section.get(reg, default) + + return default + + def setRegistry(self, reg, value, sec=None): + if sec and sec not in self._regs: + self._regs[sec] = {} + self._regs[sec][reg] = value + + def clearRegistry(self, reg, sec=None): + del self._regs[sec][reg] + + def getGlobal(self, glbl, default=None): + return self._globals.get(glbl, default) + + def getCapabilities(self): + return '' + + def LOG(self, msg): + print('PlexNet.API: {0}'.format(msg)) + + def DEBUG_LOG(self, msg): + self.LOG('DEBUG: {0}'.format(msg)) + + def WARN_LOG(self, msg): + self.LOG('WARNING: {0}'.format(msg)) + + def ERROR_LOG(self, msg): + self.LOG('ERROR: {0}'.format(msg)) + + def ERROR(self, msg=None, err=None): + if err: + self.LOG('ERROR: {0} - {1}'.format(msg, err.message)) + else: + import traceback + traceback.print_exc() + + +def createTimer(timeout, function, repeat=False, *args, **kwargs): + if isinstance(function, six.string_types): + def dummy(*args, **kwargs): + pass + dummy.__name__ = function + function = dummy + timer = util.TIMER(timeout / 1000.0, function, repeat=repeat, *args, **kwargs) + return timer + + +def setUserAgent(agent): + util.USER_AGENT = agent + util.BASE_HEADERS = util.resetBaseHeaders() + + +def setAbortFlagFunction(func): + from . import asyncadapter + asyncadapter.ABORT_FLAG_FUNCTION = func + + +def refreshResources(force=False): + from . import gdm + gdm.DISCOVERY.discover() + util.MANAGER.refreshResources(force) + SERVERMANAGER.refreshManualConnections() + + +util.setApp(App()) +util.setInterface(DumbInterface()) diff --git a/script.plexmod/lib/_included_packages/plexnet/plexconnection.py b/script.plexmod/lib/_included_packages/plexnet/plexconnection.py new file mode 100644 index 000000000..617a2d146 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexconnection.py @@ -0,0 +1,284 @@ +from __future__ import absolute_import +import random +import socket + +from . import http +from . import callback +from . import util + +HAS_ICMPLIB = False +try: + from icmplib import ping, resolve, ICMPLibError +except: + pass +else: + HAS_ICMPLIB = True + from urllib.parse import urlparse + from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network + + # local networks + LOCAL_NETWORKS = { + 4: [IPv4Network('10.0.0.0/8'), IPv4Network('192.168.0.0/16'), IPv4Network('127.0.0.0/8')], + 6: [IPv6Network('fd00::/8')] + } + + +class ConnectionSource(int): + def init(self, name): + self.name = name + return self + + def __repr__(self): + return self.name + + +class PlexConnection(object): + # Constants + STATE_UNKNOWN = "unknown" + STATE_UNREACHABLE = "unreachable" + STATE_REACHABLE = "reachable" + STATE_UNAUTHORIZED = "unauthorized" + STATE_INSECURE = "insecure_untested" + + SOURCE_MANUAL = ConnectionSource(1).init('MANUAL') + SOURCE_DISCOVERED = ConnectionSource(2).init('DISCOVERED') + SOURCE_MANUAL_AND_DISCOVERED = ConnectionSource(3).init('MANUAL, DISCOVERED') + SOURCE_MYPLEX = ConnectionSource(4).init('MYPLEX') + SOURCE_MANUAL_AND_MYPLEX = ConnectionSource(5).init('MANUAL, MYPLEX') + SOURCE_DISCOVERED_AND_MYPLEX = ConnectionSource(6).init('DISCOVERED, MYPLEX') + SOURCE_ALL = ConnectionSource(7).init('ALL') + + SCORE_REACHABLE = 4 + SCORE_LOCAL = 2 + SCORE_SECURE = 1 + + SOURCE_BY_VAL = { + 1: SOURCE_MANUAL, + 2: SOURCE_DISCOVERED, + 3: SOURCE_MANUAL_AND_DISCOVERED, + 4: SOURCE_MYPLEX, + 5: SOURCE_MANUAL_AND_MYPLEX, + 6: SOURCE_DISCOVERED_AND_MYPLEX, + 7: SOURCE_ALL + } + + def __init__(self, source, address, isLocal, token, isFallback=False, skipLocalCheck=False): + self.state = self.STATE_UNKNOWN + self.sources = source + self.address = address + self.isLocal = isLocal + self.isSecure = address[:5] == 'https' + self.isFallback = isFallback + self.token = token + self.refreshed = True + self.score = 0 + self.request = None + + self.lastTestedAt = 0 + self.hasPendingRequest = False + + self.isSecureButLocal = False + + if not HAS_ICMPLIB: + util.WARN_LOG("icmplib not found, can't check local connectivity") + + # check whether hostname is on LAN + if HAS_ICMPLIB and util.CHECK_LOCAL and not skipLocalCheck: + self.checkLocal() + + self.getScore(True) + + def __eq__(self, other): + if not other: + return False + if self.__class__ != other.__class__: + return False + return self.address == other.address + + def __ne__(self, other): + return not self.__eq__(other) + + def __str__(self): + return "Connection: {0} local: {1} token: {2} sources: {3} state: {4} score: {5}".format( + self.address, + self.isLocal, + util.hideToken(self.token), + repr(self.sources), + self.state, + self.getScore() + ) + + def __repr__(self): + return self.__str__() + + def ipInLocalNet(self, ip): + key = ":" in ip and 6 or 4 + addr = key == 4 and IPv4Address(ip) or IPv6Address(ip) + for network in LOCAL_NETWORKS[key]: + if addr in network: + return network + return False + + def checkLocal(self): + pUrl = urlparse(self.address) + hostname = pUrl.hostname + try: + ips = resolve(hostname) + except (socket.gaierror, ICMPLibError): + return False + + for ip in ips: + if ip == hostname: + continue + + network = self.ipInLocalNet(ip) + if not network: + continue + + try: + host = ping(ip, count=1, interval=1, timeout=util.LAN_REACHABILITY_TIMEOUT, privileged=False) + except: + continue + + if host.is_alive: + self.isLocal = True + util.LOG("Found IP {0} in local network ({1}) when checking {2}. Ping: {3}ms (max: {4}ms)" + .format(ip, network, self.address, host.max_rtt, int(util.LAN_REACHABILITY_TIMEOUT * 1000))) + + if self.isSecure: + # alert the server that we've found the IP locally, so we can test non-secure connectivity + self.isSecureButLocal = (ip, pUrl.port) + + return False + + def merge(self, other): + # plex.tv trumps all, otherwise assume newer is better + # ROKU: if (other.sources and self.SOURCE_MYPLEX) <> 0 then + if other.sources == self.SOURCE_MYPLEX: + self.token = other.token + else: + self.token = self.token or other.token + + self.address = other.address + self.sources = self.SOURCE_BY_VAL[self.sources | other.sources] + self.isLocal = self.isLocal | other.isLocal + self.isSecure = other.isSecure + self.isFallback = self.isFallback or other.isFallback + self.refreshed = True + + self.getScore(True) + + def testReachability(self, server, allowFallback=False): + # Check if we will allow the connection test. If this is a fallback connection, + # then we will defer it until we "allowFallback" (test insecure connections + # after secure tests have completed and failed). Insecure connections will be + # tested if the policy "always" allows them, or if set to "same_network" and + # the current connection is local and server has (publicAddressMatches=1). + insecurePolicy = util.INTERFACE.getPreference("allow_insecure") + insecureAllowed = insecurePolicy == "always" or (insecurePolicy == "same_network" and + server.sameNetwork and self.isLocal) + + allowConnectionTest = not self.isFallback or (util.LOCAL_OVER_SECURE and insecureAllowed) + if not allowConnectionTest: + if insecureAllowed: + allowConnectionTest = allowFallback + server.hasFallback = not allowConnectionTest + util.LOG( + '{0} for {1}'.format( + allowConnectionTest and "Continuing with insecure connection testing" or "Insecure connection testing is deferred", server + ) + ) + else: + util.LOG("Insecure connections not allowed. Ignore insecure connection test for {0}".format(server)) + self.state = self.STATE_INSECURE + callable = callback.Callable(server.onReachabilityResult, [self], random.randint(0, 256)) + callable.deferCall() + return True + + if allowConnectionTest: + if not self.isSecure and not util.LOCAL_OVER_SECURE and ( + not allowFallback and + server.hasSecureConnections() or + server.activeConnection and + server.activeConnection.state != self.STATE_REACHABLE and + server.activeConnection.isSecure + ): + util.DEBUG_LOG("Invalid insecure connection test in progress") + self.request = http.HttpRequest(self.buildUrl(server, "/")) + context = self.request.createRequestContext("reachability", callback.Callable(self.onReachabilityResponse), + timeout=util.CONN_CHECK_TIMEOUT) + context.server = server + util.addPlexHeaders(self.request, server.getToken()) + self.hasPendingRequest = util.APP.startRequest(self.request, context) + util.DEBUG_LOG("Testing insecure connection test for: {0}".format(server)) + return True + + return False + + def cancelReachability(self): + if self.request: + self.request.ignoreResponse = True + self.request.cancel() + + def onReachabilityResponse(self, request, response, context): + self.hasPendingRequest = False + # It's possible we may have a result pending before we were able + # to cancel it, so we'll just ignore it. + + # if request.ignoreResponse: + # return + + if response.isSuccess(): + data = response.getBodyXml() + if data is not None and context.server.collectDataFromRoot(data): + self.state = self.STATE_REACHABLE + else: + # This is unexpected, but treat it as unreachable + util.ERROR_LOG("Unable to parse root response from {0}".format(context.server)) + self.state = self.STATE_UNREACHABLE + elif response.getStatus() == 401: + self.state = self.STATE_UNAUTHORIZED + else: + self.state = self.STATE_UNREACHABLE + + self.getScore(True) + + context.server.onReachabilityResult(self) + + def buildUrl(self, server, path, includeToken=False): + if '://' in path: + url = path + else: + url = self.address + path + + if includeToken: + # If we have a token, use it. Otherwise see if any other connections + # for this server have one. That will let us use a plex.tv token for + # something like a manually configured connection. + + token = self.token or server.getToken() + + if token: + url = http.addUrlParam(url, "X-Plex-Token=" + token) + + return url + + def simpleBuildUrl(self, server, path): + token = (self.token or server.getToken()) + param = '' + if token: + param = '&X-Plex-Token={0}'.format(token) + + return '{0}{1}{2}'.format(self.address, path, param) + + def getScore(self, recalc=False): + if recalc: + self.score = 0 + if self.state == self.STATE_REACHABLE: + self.score += self.SCORE_REACHABLE + if self.isSecure: + self.score += self.SCORE_SECURE + if self.isLocal: + self.score += self.SCORE_LOCAL + (not self.isSecure and util.LOCAL_OVER_SECURE and 2 or 0) + + return self.score diff --git a/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py b/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py new file mode 100644 index 000000000..9d93d557c --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py @@ -0,0 +1,634 @@ +# -*- coding: utf-8 -*- +""" +PlexLibrary +""" +from __future__ import absolute_import +import re +from . import plexobjects +from . import playlist +from . import media +from . import exceptions +from . import util +from . import signalsmixin +from six.moves import map + + +class Library(plexobjects.PlexObject): + def __repr__(self): + return ''.format(self.title1.encode('utf8')) + + def sections(self): + items = [] + + path = '/library/sections' + for elem in self.server.query(path): + stype = elem.attrib['type'] + if stype in SECTION_TYPES: + cls = SECTION_TYPES[stype] + items.append(cls(elem, initpath=path, server=self.server, container=self)) + return items + + def section(self, title=None): + for item in self.sections(): + if item.title == title: + return item + raise exceptions.NotFound('Invalid library section: %s' % title) + + def all(self): + return plexobjects.listItems(self.server, '/library/all') + + def onDeck(self): + return plexobjects.listItems(self.server, '/library/onDeck') + + def recentlyAdded(self): + return plexobjects.listItems(self.server, '/library/recentlyAdded') + + def get(self, title): + return plexobjects.findItem(self.server, '/library/all', title) + + def getByKey(self, key): + return plexobjects.findKey(self.server, key) + + def search(self, title, libtype=None, **kwargs): + """ Searching within a library section is much more powerful. It seems certain attributes on the media + objects can be targeted to filter this search down a bit, but I havent found the documentation for + it. For example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items + such as actor= seem to work, but require you already know the id of the actor. + TLDR: This is untested but seems to work. Use library section search when you can. + """ + args = {} + if title: + args['title'] = title + if libtype: + args['type'] = plexobjects.searchType(libtype) + for attr, value in kwargs.items(): + args[attr] = value + query = '/library/all%s' % util.joinArgs(args) + return plexobjects.listItems(self.server, query) + + def cleanBundles(self): + self.server.query('/library/clean/bundles') + + def emptyTrash(self): + for section in self.sections(): + section.emptyTrash() + + def optimize(self): + self.server.query('/library/optimize') + + def refresh(self): + self.server.query('/library/sections/all/refresh') + + def randomArts(self): + return plexobjects.listItems(self.server, '/library/arts?sort=random&type=1%2c2%2c8&X-Plex-Container-Start=0&X-Plex-Container-Size=50') + + +class LibrarySection(plexobjects.PlexObject): + ALLOWED_FILTERS = () + ALLOWED_SORT = () + BOOLEAN_FILTERS = ('unwatched', 'duplicate') + + isLibraryPQ = True + + def __repr__(self): + title = self.title.replace(' ', '.')[0:20] + return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8')) + + @staticmethod + def fromFilter(filter_): + cls = SECTION_IDS.get(filter_.getLibrarySectionType()) + if not cls: + return + section = cls(None, initpath=filter_.initpath, server=filter_.server, container=filter_.container) + section.key = filter_.getLibrarySectionId() + section.title = filter_.reasonTitle + section.type = cls.TYPE + return section + + def reload(self, **kwargs): + """ Reload the data for this object from PlexServer XML. """ + initpath = '/library/sections/{0}'.format(self.key) + key = self.key + try: + data = self.server.query(initpath, params=kwargs) + except Exception as e: + import traceback + traceback.print_exc() + util.ERROR(err=e) + self.initpath = self.key + return + + self._setData(data[0]) + self.initpath = self.key = key + + def isDirectory(self): + return True + + def isLibraryItem(self): + return True + + def getAbsolutePath(self, key): + if key == 'key': + return '/library/sections/{0}/all'.format(self.key) + + return plexobjects.PlexObject.getAbsolutePath(self, key) + + def all(self, start=None, size=None, filter_=None, sort=None, unwatched=False, type_=None): + if self.key.startswith('/'): + path = '{0}/all'.format(self.key) + else: + path = '/library/sections/{0}/all'.format(self.key) + + return self.items(path, start, size, filter_, sort, unwatched, type_, False) + + def folder(self, start=None, size=None, subDir=False): + if self.key.startswith('/'): + path = self.key + else: + path = '/library/sections/{0}'.format(self.key) + + if not subDir: + path = '{0}/folder'.format(path) + + return self.items(path, start, size, None, None, False, None, True) + + def items(self, path, start, size, filter_, sort, unwatched, type_, tag_fallback): + + args = {"includeCollections" : "1"} + + if size is not None: + args['X-Plex-Container-Start'] = start + args['X-Plex-Container-Size'] = size + + if filter_: + args[filter_[0]] = filter_[1] + + if sort: + args['sort'] = '{0}:{1}'.format(*sort) + + if type_: + args['type'] = str(type_) + + if unwatched: + args[self.TYPE == 'movie' and 'unwatched' or 'unwatchedLeaves'] = 1 + + if args: + path += util.joinArgs(args, '?' not in path) + + return plexobjects.listItems(self.server, path, tag_fallback=tag_fallback) + + def jumpList(self, filter_=None, sort=None, unwatched=False, type_=None): + if self.key.startswith('/'): + path = '{0}/firstCharacter'.format(self.key) + else: + path = '/library/sections/{0}/firstCharacter'.format(self.key) + + args = {"includeCollections" : "1"} + + if filter_: + args[filter_[0]] = filter_[1] + + if sort: + args['sort'] = '{0}:{1}'.format(*sort) + + if type_: + args['type'] = str(type_) + + if unwatched: + args[self.TYPE == 'movie' and 'unwatched' or 'unwatchedLeaves'] = 1 + + if args: + path += util.joinArgs(args) + + try: + return plexobjects.listItems(self.server, path, bytag=True) + except exceptions.BadRequest: + util.ERROR('jumpList() request error for path: {0}'.format(repr(path))) + return None + + @property + def onDeck(self): + return plexobjects.listItems(self.server, '/library/sections/%s/onDeck' % self.key) + + def analyze(self): + self.server.query('/library/sections/%s/analyze' % self.key) + + def emptyTrash(self): + self.server.query('/library/sections/%s/emptyTrash' % self.key) + + def refresh(self): + self.server.query('/library/sections/%s/refresh' % self.key) + + def listChoices(self, category, libtype=None, **kwargs): + """ List choices for the specified filter category. kwargs can be any of the same + kwargs in self.search() to help narrow down the choices to only those that + matter in your current context. + """ + if category in kwargs: + raise exceptions.BadRequest('Cannot include kwarg equal to specified category: %s' % category) + args = {} + for subcategory, value in kwargs.items(): + args[category] = self._cleanSearchFilter(subcategory, value) + if libtype is not None: + args['type'] = plexobjects.searchType(libtype) + query = '/library/sections/%s/%s%s' % (self.key, category, util.joinArgs(args)) + + return plexobjects.listItems(self.server, query, bytag=True) + + def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs): + """ Search the library. If there are many results, they will be fetched from the server + in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only looking for the first + results, it would be wise to set the maxresults option to that amount so this functions + doesn't iterate over all results on the server. + title: General string query to search for. + sort: column:dir; column can be any of {addedAt, originallyAvailableAt, lastViewedAt, + titleSort, rating, mediaHeight, duration}. dir can be asc or desc. + maxresults: Only return the specified number of results + libtype: Filter results to a spcifiec libtype {movie, show, episode, artist, album, track} + kwargs: Any of the available filters for the current library section. Partial string + matches allowed. Multiple matches OR together. All inputs will be compared with the + available options and a warning logged if the option does not appear valid. + 'unwatched': Display or hide unwatched content (True, False). [all] + 'duplicate': Display or hide duplicate items (True, False). [movie] + 'actor': List of actors to search ([actor_or_id, ...]). [movie] + 'collection': List of collections to search within ([collection_or_id, ...]). [all] + 'contentRating': List of content ratings to search within ([rating_or_key, ...]). [movie, tv] + 'country': List of countries to search within ([country_or_key, ...]). [movie, music] + 'decade': List of decades to search within ([yyy0, ...]). [movie] + 'director': List of directors to search ([director_or_id, ...]). [movie] + 'genre': List Genres to search within ([genere_or_id, ...]). [all] + 'network': List of TV networks to search within ([resolution_or_key, ...]). [tv] + 'resolution': List of video resolutions to search within ([resolution_or_key, ...]). [movie] + 'studio': List of studios to search within ([studio_or_key, ...]). [music] + 'year': List of years to search within ([yyyy, ...]). [all] + """ + # Cleanup the core arguments + args = {} + for category, value in kwargs.items(): + args[category] = self._cleanSearchFilter(category, value, libtype) + if title is not None: + args['title'] = title + if sort is not None: + args['sort'] = self._cleanSearchSort(sort) + if libtype is not None: + args['type'] = plexobjects.searchType(libtype) + # Iterate over the results + results, subresults = [], '_init' + args['X-Plex-Container-Start'] = 0 + args['X-Plex-Container-Size'] = min(util.X_PLEX_CONTAINER_SIZE, maxresults) + while subresults and maxresults > len(results): + query = '/library/sections/%s/all%s' % (self.key, util.joinArgs(args)) + subresults = plexobjects.listItems(self.server, query) + results += subresults[:maxresults - len(results)] + args['X-Plex-Container-Start'] += args['X-Plex-Container-Size'] + return results + + def _cleanSearchFilter(self, category, value, libtype=None): + # check a few things before we begin + if category not in self.ALLOWED_FILTERS: + raise exceptions.BadRequest('Unknown filter category: %s' % category) + if category in self.BOOLEAN_FILTERS: + return '1' if value else '0' + if not isinstance(value, (list, tuple)): + value = [value] + # convert list of values to list of keys or ids + result = set() + choices = self.listChoices(category, libtype) + lookup = {} + for c in choices: + lookup[c.title.lower()] = c.key + + allowed = set(c.key for c in choices) + for item in value: + item = str(item.id if isinstance(item, media.MediaTag) else item).lower() + # find most logical choice(s) to use in url + if item in allowed: + result.add(item) + continue + if item in lookup: + result.add(lookup[item]) + continue + matches = [k for t, k in lookup.items() if item in t] + if matches: + list(map(result.add, matches)) + continue + # nothing matched; use raw item value + util.LOG('Filter value not listed, using raw item value: {0}'.format(item)) + result.add(item) + return ','.join(result) + + def _cleanSearchSort(self, sort): + sort = '%s:asc' % sort if ':' not in sort else sort + scol, sdir = sort.lower().split(':') + lookup = {} + for s in self.ALLOWED_SORT: + lookup[s.lower()] = s + if scol not in lookup: + raise exceptions.BadRequest('Unknown sort column: %s' % scol) + if sdir not in ('asc', 'desc'): + raise exceptions.BadRequest('Unknown sort dir: %s' % sdir) + return '%s:%s' % (lookup[scol], sdir) + + +class MovieSection(LibrarySection): + ALLOWED_FILTERS = ( + 'unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating', 'collection', + 'director', 'actor', 'country', 'studio', 'resolution' + ) + ALLOWED_SORT = ( + 'addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating', 'audienceRating', 'userRating', + 'contentRating', 'mediaHeight', 'duration' + ) + TYPE = 'movie' + ID = '1' + + +class ShowSection(LibrarySection): + ALLOWED_FILTERS = ('unwatched', 'year', 'genre', 'contentRating', 'network', 'collection') + ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'originallyAvailableAt', 'titleSort', 'rating', 'audienceRating', + 'userRating', 'contentRating', 'unwatched') + TYPE = 'show' + ID = '2' + + def searchShows(self, **kwargs): + return self.search(libtype='show', **kwargs) + + def searchEpisodes(self, **kwargs): + return self.search(libtype='episode', **kwargs) + + +class MusicSection(LibrarySection): + ALLOWED_FILTERS = ('genre', 'country', 'collection') + ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort') + TYPE = 'artist' + ID = '8' + + def searchShows(self, **kwargs): + return self.search(libtype='artist', **kwargs) + + def searchEpisodes(self, **kwargs): + return self.search(libtype='album', **kwargs) + + def searchTracks(self, **kwargs): + return self.search(libtype='track', **kwargs) + + +class PhotoSection(LibrarySection): + ALLOWED_FILTERS = () + ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort') + TYPE = 'photo' + ID = 'None' + + def isPhotoOrDirectoryItem(self): + return True + + +@plexobjects.registerLibType +class Collection(media.MediaItem): + TYPE = 'collection' + + def __repr__(self): + title = self.title.replace(' ', '.')[0:20] + return '<{0}:{1}:{2}>'.format(self.__class__.__name__, self.key, title) + + def all(self, *args, **kwargs): + items = plexobjects.listItems(self.server, self.key) + items.totalSize = items.size + return items + + def isMusicOrDirectoryItem(self): + return self.container.viewGroup in ('artist', 'album', 'track') + + def isVideoOrDirectoryItem(self): + return self.container.viewGroup in ('movie', 'show', 'episode') + + def isCollection(self): + return True + + +@plexobjects.registerLibType +class Generic(plexobjects.PlexObject): + TYPE = 'Directory' + + def __repr__(self): + title = self.title.replace(' ', '.')[0:20] + return '<{0}:{1}:{2}>'.format(self.__class__.__name__, self.key, title) + +#@plexobjects.registerLibType +#class Collection(Generic): +# TYPE = 'collection' + +@plexobjects.registerLibType +class Playlist(playlist.BasePlaylist, signalsmixin.SignalsMixin): + TYPE = 'playlist' + + def __init__(self, *args, **kwargs): + playlist.BasePlaylist.__init__(self, *args, **kwargs) + signalsmixin.SignalsMixin.__init__(self) + self._itemsLoaded = False + + def __repr__(self): + title = self.title.replace(' ', '.')[0:20] + return '<{0}:{1}:{2}>'.format(self.__class__.__name__, self.key, title) + + def exists(self): + try: + self.server.query('/playlists/{0}'.format(self.ratingKey)) + return True + except exceptions.BadRequest: + return False + + def isMusicOrDirectoryItem(self): + return self.playlistType == 'audio' + + def isVideoOrDirectoryItem(self): + return self.playlistType == 'video' + + def items(self): + if not self._itemsLoaded: + path = '/playlists/{0}/items'.format(self.ratingKey) + self._items = plexobjects.listItems(self.server, path) + self._itemsLoaded = True + + return playlist.BasePlaylist.items(self) + + def extend(self, start=0, size=0): + if not self._items: + self._items = [None] * self.leafCount.asInt() + + args = {} + + if size is not None: + args['X-Plex-Container-Start'] = start + args['X-Plex-Container-Size'] = size + + path = '/playlists/{0}/items'.format(self.ratingKey) + if args: + path += util.joinArgs(args) if '?' not in path else '&' + util.joinArgs(args).lstrip('?') + + items = plexobjects.listItems(self.server, path) + self._items[start:start + len(items)] = items + + self.trigger('items.added') + + return items + + def unshuffledItems(self): + if not self._itemsLoaded: + list(self.items()) + return self._items + + @property + def defaultThumb(self): + return self.composite + + def buildComposite(self, **kwargs): + if kwargs: + params = '?' + '&'.join('{0}={1}'.format(k, v) for k, v in kwargs.items()) + else: + params = '' + + path = self.composite + params + return self.getServer().buildUrl(path, True) + + +class BaseHub(plexobjects.PlexObject): + def reset(self): + self.set('offset', 0) + self.set('size', len(self.items)) + totalSize = self.items[0].container.totalSize.asInt() + if totalSize: # Hubs from a list of hubs don't have this, so it it's not here this is intital and we can leave as is + self.set( + 'more', + (self.items[0].container.offset.asInt() + self.items[0].container.size.asInt() < totalSize) and '1' or '' + ) + + +class Hub(BaseHub): + TYPE = "Hub" + + def init(self, data): + self.items = [] + + container = plexobjects.PlexContainer(data, self.key, self.server, self.key or '') + + if self.type == 'genre': + self.items = [media.Genre(elem, initpath='/hubs', server=self.server, container=container) for elem in data] + elif self.type == 'director': + self.items = [media.Director(elem, initpath='/hubs', server=self.server, container=container) for elem in data] + elif self.type == 'actor': + self.items = [media.Role(elem, initpath='/hubs', server=self.server, container=container) for elem in data] + else: + for elem in data: + try: + self.items.append(plexobjects.buildItem(self.server, elem, '/hubs', container=container, tag_fallback=True)) + except exceptions.UnknownType: + util.DEBUG_LOG('Unkown hub item type({1}): {0}'.format(elem, elem.attrib.get('type'))) + + def __repr__(self): + return '<{0}:{1}>'.format(self.__class__.__name__, self.hubIdentifier) + + def getCleanHubIdentifier(self): + return re.sub(r'\.\d+$', '', re.sub(r'\.\d+$', '', self.hubIdentifier)) + + def reload(self, **kwargs): + """ Reload the data for this object from PlexServer XML. """ + try: + data = self.server.query(self.key, params=kwargs) + except Exception as e: + import traceback + traceback.print_exc() + util.ERROR(err=e) + self.initpath = self.key + return + + self.initpath = self.key + self._setData(data) + self.init(data) + + def extend(self, start=None, size=None): + path = self.key + + args = {} + + if size is not None: + args['X-Plex-Container-Start'] = start + args['X-Plex-Container-Size'] = size + + if args: + path += util.joinArgs(args) if '?' not in path else '&' + util.joinArgs(args).lstrip('?') + + items = plexobjects.listItems(self.server, path) + self.offset = plexobjects.PlexValue(start) + self.size = plexobjects.PlexValue(len(items)) + self.more = plexobjects.PlexValue('') + if items: + self.more = plexobjects.PlexValue( + (items[0].container.offset.asInt() + items[0].container.size.asInt() < items[0].container.totalSize.asInt()) and '1' or '' + ) + return items + + +class PlaylistHub(BaseHub): + TYPE = "Hub" + type = None + hubIdentifier = None + + def init(self, data): + try: + self.items = self.extend(0, 10) + except exceptions.BadRequest: + util.DEBUG_LOG('AudioPlaylistHub: Bad request: {0}'.format(self)) + self.items = [] + + def getCleanHubIdentifier(self): + return re.sub(r'\.\d+$', '', re.sub(r'\.\d+$', '', self.hubIdentifier)) + + def extend(self, start=None, size=None): + path = '/playlists/all?playlistType={0}'.format(self.type) + + args = {"includeMarkers": 1} + + if size is not None: + args['X-Plex-Container-Start'] = start + args['X-Plex-Container-Size'] = size + else: + start = 0 + + if args: + path += '&' + util.joinArgs(args).lstrip('?') + + items = plexobjects.listItems(self.server, path) + + if not items: + return + + self.set('offset', start) + self.set('size', len(items)) + self.set('more', (items[0].container.offset.asInt() + items[0].container.size.asInt() < items[0].container.totalSize.asInt()) and '1' or '') + return items + + +class AudioPlaylistHub(PlaylistHub): + type = 'audio' + hubIdentifier = 'playlists.audio' + + +class VideoPlaylistHub(PlaylistHub): + type = 'video' + hubIdentifier = 'playlists.video' + + +SECTION_TYPES = { + MovieSection.TYPE: MovieSection, + ShowSection.TYPE: ShowSection, + MusicSection.TYPE: MusicSection, + PhotoSection.TYPE: PhotoSection +} + +SECTION_IDS = { + MovieSection.ID: MovieSection, + ShowSection.ID: ShowSection, + MusicSection.ID: MusicSection, + PhotoSection.ID: PhotoSection +} diff --git a/script.plexmod/lib/_included_packages/plexnet/plexmedia.py b/script.plexmod/lib/_included_packages/plexnet/plexmedia.py new file mode 100644 index 000000000..bbbc51825 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexmedia.py @@ -0,0 +1,154 @@ +from __future__ import absolute_import +from . import locks +from . import http +from . import plexobjects +from . import plexpart +from . import plexrequest +from . import util +from six.moves import filter +import six + + +class PlexMedia(plexobjects.PlexObject): + def __init__(self, data, initpath=None, server=None, container=None): + self._data = data.attrib + plexobjects.PlexObject.__init__(self, data, initpath, server) + self.container_ = self.get('container') + self.container = container + self.indirectHeaders = None + self.parts = [] + # If we weren't given any data, this is a synthetic media + if data is not None: + self.parts = [plexpart.PlexPart(elem, initpath=self.initpath, server=self.server, media=self) for elem in data] + + def get(self, key, default=None): + return self._data.get(key, default) + + def hasStreams(self): + return len(self.parts) > 0 and self.parts[0].hasStreams() + + def isIndirect(self): + return self.get('indirect') == '1' + + def isAccessible(self): + return any(p.isAccessible() for p in self.parts) + + def isAvailable(self): + return any(p.isAvailable() for p in self.parts) + + def resolveIndirect(self): + if not self.isIndirect() or locks.LOCKS.isLocked("resolve_indirect"): + return self + + part = self.parts[0] + if part is None: + util.DEBUG("Failed to resolve indirect media: missing valid part") + return None + + postBody = None + postUrl = part.postURL + request = plexrequest.PlexRequest(self.getServer(), part.key, postUrl is not None and "POST" or "GET") + + if postUrl is not None: + util.DEBUG("Fetching content for indirect media POST URL: {0}".format(postUrl)) + # Force setting the certificate to handle following https redirects + postRequest = http.HttpRequest(postUrl, None, True) + postResponse = postRequest.getToStringWithTimeout(30) + if len(postResponse) > 0 and type(postRequest.event) == "roUrlEvent": + util.DEBUG("Retrieved data from postURL, posting to resolve container") + crlf = chr(13) + chr(10) + postBody = "" + for header in postRequest.event.getResponseHeadersArray(): + for name in header: + postBody = postBody + name + ": " + header[name] + crlf + postBody = postBody + crlf + postResponse + else: + util.DEBUG("Failed to resolve indirect media postUrl") + self.Set("indirect", "-1") + return self + + request.addParam("postURL", postUrl) + + response = request.doRequestWithTimeout(30, postBody) + + item = response.items[0] + if item is None or item.mediaItems[0] is None: + util.DEBUG("Failed to resolve indirect media: no media items") + self.indirect = -1 + return self + + media = item.mediaItems[0] + + # Add indirect headers to the media item + media.indirectHeaders = util.AttributeDict() + for header in (item.container.httpHeaders or '').split("&"): + arr = header.split("=") + if len(arr) == 2: + media.indirectHeaders[arr[0]] = arr[1] + + # Reset the fallback media id if applicable + if self.id.asInt() < 0: + media.id = self.id + + return media.resolveIndirect() + + def __str__(self): + extra = [] + attrs = ("videoCodec", "audioCodec", "audioChannels", "protocol", "id") + if self.get('container'): + extra.append("container={0}".format(self.get('container'))) + + for astr in attrs: + if hasattr(self, astr): + attr = getattr(self, astr) + if attr and not attr.NA: + extra.append("{0}={1}".format(astr, attr)) + + return self.versionString(log_safe=True) + " " + ' '.join(extra) + + def versionString(self, log_safe=False): + details = [] + details.append(self.getVideoResolutionString()) + if self.bitrate.asInt() > 0: + details.append(util.bitrateToString(self.bitrate.asInt() * 1000)) + + detailString = ', '.join(details) + return (log_safe and ' * ' or u" \u2022 ").join([_f for _f in [self.title, detailString] if _f]) + + def __eq__(self, other): + if not other: + return False + + if self.__class__ != other.__class__: + return False + + return self.id == other.id + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return self.__str__() + + def getVideoResolution(self): + if self.videoResolution: + standardDefinitionHeight = 480 + if str(util.validInt(list(filter(six.text_type.isdigit, self.videoResolution)))) != self.videoResolution: + return self.height.asInt() > standardDefinitionHeight and self.height.asInt() or standardDefinitionHeight + else: + return self.videoResolution.asInt(standardDefinitionHeight) + + return self.height.asInt() + + def getVideoResolutionString(self): + resNumber = util.validInt(list(filter(six.text_type.isdigit, self.videoResolution))) + if resNumber > 0 and str(resNumber) == self.videoResolution: + return self.videoResolution + "p" + + return self.videoResolution.upper() + + def isSelected(self): + from . import plexapp + return self.selected.asBool() or self.id == util.INTERFACE.getPreference("local_mediaId") + + # TODO(schuyler): getParts diff --git a/script.plexmod/lib/_included_packages/plexnet/plexobjects.py b/script.plexmod/lib/_included_packages/plexnet/plexobjects.py new file mode 100644 index 000000000..6d5c01f2d --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexobjects.py @@ -0,0 +1,572 @@ +from __future__ import absolute_import +from datetime import datetime + +from . import exceptions +from . import util +import json +import six +import time + +# Search Types - Plex uses these to filter specific media types when searching. +SEARCHTYPES = { + 'movie': 1, + 'show': 2, + 'season': 3, + 'episode': 4, + 'artist': 8, + 'album': 9, + 'track': 10 +} + +LIBRARY_TYPES = {} + + +def registerLibType(cls): + LIBRARY_TYPES[cls.TYPE] = cls + return cls + + +def registerLibFactory(ftype): + def wrap(func): + LIBRARY_TYPES[ftype] = func + return func + return wrap + + +class PlexValue(six.text_type): + def __new__(cls, value, parent=None): + self = super(PlexValue, cls).__new__(cls, value) + self.parent = parent + self.NA = False + return self + + def __call__(self, default): + return not self.NA and self or PlexValue(default, self.parent) + + def asBool(self): + return self == '1' + + def asInt(self, default=0): + return int(self or default) + + def asFloat(self, default=0): + return float(self or default) + + def asDatetime(self, format_=None): + if not self: + return None + + if self.isdigit(): + dt = datetime.fromtimestamp(int(self)) + else: + # dt = datetime.strptime(self, '%Y-%m-%d') + # Avoid datetime.strptime to avoid + # https://github.com/python/cpython/issues/71587 + try: + dt = datetime.fromtimestamp(time.mktime(time.strptime(self, '%Y-%m-%d'))) + except OverflowError: + # special case for dates before 1970-01-02 (yes, there are shows that old), mktime fails on those + year, month, day = (int(p) for p in str(self).split("-")) + dt = datetime(year=year, month=month, day=day) + + if not format_: + return dt + + return dt.strftime(format_) + + def asURL(self, includeToken=False): + return self.parent.server.buildUrl(self, includeToken) + + def asTranscodedImageURL(self, w, h, **extras): + return self.parent.server.getImageTranscodeURL(self, w, h, **extras) + + +class JEncoder(json.JSONEncoder): + def default(self, o): + try: + return json.JSONEncoder.default(self, o) + except: + return None + + +def asFullObject(func): + def wrap(self, *args, **kwargs): + if not self.isFullObject(): + self.reload() + return func(self, *args, **kwargs) + + return wrap + + +class Checks(object): + def isLibraryItem(self): + return "/library/metadata" in self.get('key', '') or ("/playlists/" in self.get('key', '') and self.get("type", "") == "playlist") + + def isVideoItem(self): + return False + + def isMusicItem(self): + return False + + def isOnlineItem(self): + return self.isChannelItem() or self.isMyPlexItem() or self.isVevoItem() or self.isIvaItem() + + def isMyPlexItem(self): + return self.container.server.TYPE == 'MYPLEXSERVER' or self.container.identifier == 'com.plexapp.plugins.myplex' + + def isChannelItem(self): + identifier = self.getIdentifier() or "com.plexapp.plugins.library" + return not self.isLibraryItem() and not self.isMyPlexItem() and identifier != "com.plexapp.plugins.library" + + def isVevoItem(self): + return 'vevo://' in self.get('guid') + + def isIvaItem(self): + return 'iva://' in self.get('guid') + + def isGracenoteCollection(self): + return False + + def isIPhoto(self): + return (self.title == "iPhoto" or self.container.title == "iPhoto" or (self.mediaType == "Image" or self.mediaType == "Movie")) + + def isDirectory(self): + return self.name == "Directory" or self.name == "Playlist" + + def isPhotoOrDirectoryItem(self): + return self.type == "photoalbum" # or self.isPhotoItem() + + def isMusicOrDirectoryItem(self): + return self.type in ('artist', 'album', 'track') + + def isVideoOrDirectoryItem(self): + return self.type in ('movie', 'show', 'episode') + + def isSettings(self): + return False + + +class PlexObject(Checks): + def __init__(self, data, initpath=None, server=None, container=None): + self.initpath = initpath + self.key = None + self.server = server + self.container = container + self.mediaChoice = None + self.titleSort = PlexValue('') + self.deleted = False + self._reloaded = False + self.data = data + + if data is None: + return + + self._setData(data) + + self.init(data) + + def _setData(self, data): + if data is False: + return + + self.name = data.tag + for k, v in data.attrib.items(): + if k in ("container",): + k = "attrib_%s" % k + + setattr(self, k, PlexValue(v, self)) + + def __getattr__(self, attr): + a = PlexValue('', self) + a.NA = True + + try: + setattr(self, attr, a) + except AttributeError: + util.LOG('Failed to set attribute: {0} ({1})'.format(attr, self.__class__)) + + return a + + def exists(self): + # Used for media items - for others we just return True + return True + + def get(self, attr, default=''): + ret = self.__dict__.get(attr) + return ret is not None and ret or PlexValue(default, self) + + def set(self, attr, value): + setattr(self, attr, PlexValue(six.text_type(value), self)) + + def init(self, data): + pass + + def isFullObject(self): + return self.initpath is None or self.key is None or self.initpath == self.key + + def getAddress(self): + return self.server.activeConnection.address + + @property + def defaultTitle(self): + return self.get('title') + + @property + def defaultThumb(self): + return self.__dict__.get('thumb') and self.thumb or PlexValue('', self) + + @property + def defaultArt(self): + return self.__dict__.get('art') and self.art or PlexValue('', self) + + def refresh(self): + import requests + self.server.query('%s/refresh' % self.key, method=requests.put) + + def reload(self, _soft=False, **kwargs): + """ Reload the data for this object from PlexServer XML. """ + if _soft and self._reloaded: + return self + + kwargs["includeMarkers"] = 1 + + try: + if self.get('ratingKey'): + data = self.server.query('/library/metadata/{0}'.format(self.ratingKey), params=kwargs) + else: + data = self.server.query(self.key, params=kwargs) + self._reloaded = True + except Exception as e: + import traceback + traceback.print_exc() + util.ERROR(err=e) + self.initpath = self.key + return self + + self.initpath = self.key + + try: + self._setData(data[0]) + except (IndexError, TypeError, AttributeError): + util.DEBUG_LOG('No data on reload: {0}'.format(self)) + return self + + return self + + def softReload(self, **kwargs): + return self.reload(_soft=True, **kwargs) + + def getLibrarySectionId(self): + ID = self.get('librarySectionID') + + if not ID: + ID = self.container.get("librarySectionID", '') + + return ID + + def getLibrarySectionTitle(self): + title = self.get('librarySectionTitle') + + if not title: + title = self.container.get("librarySectionTitle", '') + + if not title: + lsid = self.getLibrarySectionId() + if lsid: + data = self.server.query('/library/sections/{0}'.format(lsid)) + title = data.attrib.get('title1') + if title: + self.librarySectionTitle = title + return str(title) + + def getLibrarySectionType(self): + type_ = self.get('librarySectionType') + + if not type_: + type_ = self.container.get("librarySectionType", '') + + if not type_: + lsid = self.getLibrarySectionId() + if lsid: + data = self.server.query('/library/sections/{0}'.format(lsid)) + type_ = data.attrib.get('type') + if type_: + self.librarySectionTitle = type_ + return type_ + + def getLibrarySectionUuid(self): + uuid = self.get("uuid") or self.get("librarySectionUUID") + + if not uuid: + uuid = self.container.get("librarySectionUUID", "") + + return uuid + + def _findLocation(self, data): + elem = data.find('Location') + if elem is not None: + return elem.attrib.get('path') + return None + + def _findPlayer(self, data): + elem = data.find('Player') + if elem is not None: + return PlexObject(elem, server=self.server) + return None + + def _findTranscodeSession(self, data): + elem = data.find('TranscodeSession') + if elem is not None: + from . import media + return media.TranscodeSession(elem, server=self.server) + return None + + def _findBandwidths(self, data): + elem = data.find("Bandwidths") + if elem is not None: + from . import media + return PlexItemList(elem, media.Bandwidth, media.Bandwidth.TYPE, server=self.server) + return [] + + def _findUser(self, data): + elem = data.find('User') + if elem is not None: + return PlexObject(elem, self.initpath) + return None + + def _findSession(self, data): + elem = data.find('Session') + if elem is not None: + return PlexObject(elem, self.initpath, server=self.server) + return None + + def getAbsolutePath(self, attr): + path = getattr(self, attr, None) + if path is None: + return None + else: + return self.container._getAbsolutePath(path) + + def _getAbsolutePath(self, path): + if path.startswith('/'): + return path + elif "://" in path: + return path + else: + return self.getAddress() + "/" + path + + def getParentPath(self, key): + # Some containers have /children on its key while others (such as playlists) use /items + path = self.getAbsolutePath(key) + if path is None: + return "" + + for suffix in ("/children", "/items"): + path = path.replace(suffix, "") + + return path + + def getServer(self): + return self.server + + def getTranscodeServer(self, localServerRequired=False, transcodeType=None): + server = self.server + + # If the server is myPlex, try to use a different PMS for transcoding + from . import myplexserver + from . import plexapp + if server == myplexserver.MyPlexServer: + fallbackServer = plexapp.SERVERMANAGER.getChannelServer() + + if fallbackServer: + server = fallbackServer + elif localServerRequired: + return None + + return server + + @classmethod + def deSerialize(cls, jstring): + from . import plexserver + obj = json.loads(jstring) + server = plexserver.PlexServer.deSerialize(obj['server']) + server.identifier = None + ad = util.AttributeDict() + ad.attrib = obj['obj'] + ad.find = lambda x: None + po = buildItem(server, ad, ad.initpath, container=server) + + return po + + def serialize(self, full=False): + import json + odict = {} + if full: + for k, v in self.__dict__.items(): + if k not in ('server', 'container', 'media', 'initpath', '_data') and v: + odict[k] = v + else: + odict['key'] = self.key + odict['type'] = self.type + + odict['initpath'] = '/none' + obj = {'obj': odict, 'server': self.server.serialize(full=full)} + + return json.dumps(obj, cls=JEncoder) + + +class PlexContainer(PlexObject): + def __init__(self, data, initpath=None, server=None, address=None): + PlexObject.__init__(self, data, initpath, server) + self.setAddress(address) + + def setAddress(self, address): + if address != "/" and address.endswith("/"): + self.address = address[:-1] + else: + self.address = address + + # TODO(schuyler): Do we need to make sure that we only hang onto the path here and not a full URL? + if not self.address.startswith("/") and "node.plexapp.com" not in self.address: + util.FATAL("Container address is not an expected path: {0}".format(address)) + + def getAbsolutePath(self, path): + if path.startswith('/'): + return path + elif "://" in path: + return path + else: + return self.address + "/" + path + + +class PlexServerContainer(PlexContainer): + def __init__(self, data, initpath=None, server=None, address=None): + PlexContainer.__init__(self, data, initpath, server, address) + from . import plexserver + self.resources = [plexserver.PlexServer(elem) for elem in data] + + def __getitem__(self, idx): + return self.resources[idx] + + def __iter__(self): + for i in self.resources: + yield i + + def __len__(self): + return len(self.resources) + + +class PlexItemList(object): + def __init__(self, data, item_cls, tag, server=None, container=None): + self._data = data + self._itemClass = item_cls + self._itemTag = tag + self._server = server + self._container = container + self._items = None + + def __iter__(self): + for i in self.items: + yield i + + def __getitem__(self, idx): + return self.items[idx] + + @property + def items(self): + if self._items is None: + if self._data is not None: + if self._server: + self._items = [self._itemClass(elem, server=self._server, container=self._container) for elem in self._data if elem.tag == self._itemTag] + else: + self._items = [self._itemClass(elem) for elem in self._data if elem.tag == self._itemTag] + else: + self._items = [] + + return self._items + + def __call__(self, *args): + return self.items + + def __len__(self): + return len(self.items) + + def append(self, item): + self.items.append(item) + + +class PlexMediaItemList(PlexItemList): + def __init__(self, data, item_cls, tag, initpath=None, server=None, media=None): + PlexItemList.__init__(self, data, item_cls, tag, server) + self._initpath = initpath + self._media = media + self._items = None + + @property + def items(self): + if self._items is None: + if self._data is not None: + self._items = [self._itemClass(elem, self._initpath, self._server, self._media) for elem in self._data if elem.tag == self._itemTag] + else: + self._items = [] + + return self._items + + +def findItem(server, path, title): + for elem in server.query(path): + if elem.attrib.get('title').lower() == title.lower(): + return buildItem(server, elem, path) + raise exceptions.NotFound('Unable to find item: {0}'.format(title)) + + +def buildItem(server, elem, initpath, bytag=False, container=None, tag_fallback=False): + libtype = elem.tag if bytag else elem.attrib.get('type') + if not libtype and tag_fallback: + libtype = elem.tag + + if libtype in LIBRARY_TYPES: + cls = LIBRARY_TYPES[libtype] + return cls(elem, initpath=initpath, server=server, container=container) + raise exceptions.UnknownType('Unknown library type: {0}'.format(libtype)) + + +class ItemContainer(list): + def __getattr__(self, attr): + return getattr(self.container, attr) + + def init(self, container): + self.container = container + return self + + +def listItems(server, path, libtype=None, watched=None, bytag=False, data=None, container=None, offset=None, + limit=None, tag_fallback=False, **kwargs): + data = data if data is not None else server.query(path, offset=offset, limit=limit, **kwargs) + container = container or PlexContainer(data, path, server, path) + items = ItemContainer().init(container) + + if data: + for elem in data: + if libtype and elem.attrib.get('type') != libtype: + continue + if watched is True and elem.attrib.get('viewCount', 0) == 0: + continue + if watched is False and elem.attrib.get('viewCount', 0) >= 1: + continue + try: + items.append(buildItem(server, elem, path, bytag, container, tag_fallback)) + except exceptions.UnknownType: + pass + + return items + + +def searchType(libtype): + searchtypesstrs = [str(k) for k in SEARCHTYPES.keys()] + if libtype in SEARCHTYPES + searchtypesstrs: + return libtype + stype = SEARCHTYPES.get(libtype.lower()) + if not stype: + raise exceptions.NotFound('Unknown libtype: %s' % libtype) + return stype diff --git a/script.plexmod/lib/_included_packages/plexnet/plexpart.py b/script.plexmod/lib/_included_packages/plexnet/plexpart.py new file mode 100644 index 000000000..7fd0c138c --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexpart.py @@ -0,0 +1,178 @@ +from __future__ import absolute_import +from . import plexobjects +from . import plexstream +from . import plexrequest +from . import util + + +class PlexPart(plexobjects.PlexObject): + def reload(self): + self.initpath = self.key + + def __init__(self, data, initpath=None, server=None, media=None): + plexobjects.PlexObject.__init__(self, data, initpath, server) + self.container_ = self.container + self.container = media + self.streams = [] + + # If we weren't given any data, this is a synthetic part + if data is not None: + self.streams = [plexstream.PlexStream(e, initpath=self.initpath, server=self.server) for e in data if e.tag == 'Stream'] + if self.indexes: + indexKeys = self.indexes('').split(",") + self.indexes = util.AttributeDict() + for indexKey in indexKeys: + self.indexes[indexKey] = True + + def getAddress(self): + address = self.key + + if address != "": + # TODO(schuyler): Do we need to add a token? Or will it be taken care of via header else:where? + address = self.container.getAbsolutePath(address) + + return address + + def isAccessible(self): + # If we haven't fetched accessibility info, assume it's accessible. + return self.accessible.asBool() if self.accessible else True + + def isAvailable(self): + # If we haven't fetched availability info, assume it's available + return not self.exists or self.exists.asBool() + + def getStreamsOfType(self, streamType): + streams = [] + + foundSelected = False + + for stream in self.streams: + if stream.streamType.asInt() == streamType: + streams.append(stream) + + if stream.isSelected(): + foundSelected = True + + # If this is subtitles, add the none option + if streamType == plexstream.PlexStream.TYPE_SUBTITLE: + none = plexstream.NoneStream() + streams.insert(0, none) + none.setSelected(not foundSelected) + + return streams + + # def getSelectedStreamStringOfType(self, streamType): + # default = None + # availableStreams = 0 + # for stream in self.streams: + # if stream.streamType.asInt() == streamType: + # availableStreams = availableStreams + 1 + # if stream.isSelected() or (default is None and streamType != stream.TYPE_SUBTITLE): + # default = stream + + # if default is not None: + # availableStreams = availableStreams - 1 + # title = default.getTitle() + # suffix = "More" + # else: + # title = "None" + # suffix = "Available" + + # if availableStreams > 0 and streamType != stream.TYPE_VIDEO: + # # Indicate available streams to choose from, excluding video + # # streams until the server supports multiple videos streams. + + # return u"{0} : {1} {2}".format(title, availableStreams, suffix) + # else: + # return title + + def getSelectedStreamOfType(self, streamType): + # Video streams, in particular, may not be selected. Pretend like the + # first one was selected. + + default = None + + for stream in self.streams: + if stream.streamType.asInt() == streamType: + if stream.isSelected(): + return stream + elif default is None and streamType != stream.TYPE_SUBTITLE: + default = stream + + return default + + def setSelectedStream(self, streamType, streamId, _async): + if streamType == plexstream.PlexStream.TYPE_AUDIO: + typeString = "audio" + elif streamType == plexstream.PlexStream.TYPE_SUBTITLE: + typeString = "subtitle" + elif streamType == plexstream.PlexStream.TYPE_VIDEO: + typeString = "video" + else: + return None + + path = "/library/parts/{0}?{1}StreamID={2}".format(self.id(''), typeString, streamId) + + if self.getServer().supportsFeature("allPartsStreamSelection"): + path = path + "&allParts=1" + + request = plexrequest.PlexRequest(self.getServer(), path, "PUT") + + if _async: + context = request.createRequestContext("ignored") + from . import plexapp + util.APP.startRequest(request, context, "") + else: + request.postToStringWithTimeout() + + matching = plexstream.NoneStream() + + # Update any affected streams + for stream in self.streams: + if stream.streamType.asInt() == streamType: + if stream.id == streamId: + stream.setSelected(True) + matching = stream + elif stream.isSelected(): + stream.setSelected(False) + + return matching + + def isIndexed(self): + return bool(self.indexes) + + def getIndexUrl(self, indexKey): + path = self.getIndexPath(indexKey) + if path is not None: + return self.container.server.buildUrl(path + "?interval=10000", True) + else: + return None + + def getIndexPath(self, indexKey, interval=None): + if self.indexes is not None and indexKey in self.indexes: + return "/library/parts/{0}/indexes/{1}".format(self.id, indexKey) + else: + return None + + def hasStreams(self): + return bool(self.streams) + + def __str__(self): + return "PlexPart {0} {1}".format(self.id("NaN"), self.key) + + def __eq__(self, other): + if other is None: + return False + + if self.__class__ != other.__class__: + return False + + return self.id == other.id + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return self.__str__() + + # TODO(schuyler): getStreams, getIndexThumbUrl diff --git a/script.plexmod/lib/_included_packages/plexnet/plexplayer.py b/script.plexmod/lib/_included_packages/plexnet/plexplayer.py new file mode 100644 index 000000000..e90a01025 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexplayer.py @@ -0,0 +1,934 @@ +from __future__ import absolute_import +import re +from . import util +from . import captions +from . import http +from . import plexrequest +from . import mediadecisionengine +from . import serverdecision +from lib.util import CACHE_SIZE, advancedSettings, KODI_VERSION_MAJOR + +from six.moves import range + +DecisionFailure = serverdecision.DecisionFailure + + +class PlexPlayer(object): + DECISION_ENDPOINT = "/video/:/transcode/universal/decision" + + def __init__(self, item, seekValue=0, forceUpdate=False): + self.decision = None + self.seekValue = seekValue + self.metadata = None + self.init(item, forceUpdate) + + def init(self, item, forceUpdate=False): + self.item = item + self.choice = mediadecisionengine.MediaDecisionEngine().chooseMedia(item, forceUpdate=forceUpdate) + if self.choice: + self.media = self.choice.media + + def terminate(self, code, reason): + util.LOG('TERMINATE PLAYER: ({0}, {1})'.format(code, reason)) + # TODO: Handle this? ---------------------------------------------------------------------------------------------------------- TODO + + @property + def audioChannels(self): + """ + Parse Kodi channel setting into channel count + """ + channelDef = self.item.settings.getGlobal("audioChannels", "2.0") + major, minor = channelDef.split(".") if "." in channelDef else (channelDef, 0) + return int(major) + int(minor) + + def rebuild(self, item, decision=None): + # item.settings = self.item.settings + oldChoice = self.choice + self.init(item, True) + util.LOG("Replacing '{0}' with '{1}' and rebuilding.".format(oldChoice, self.choice)) + self.build() + self.decision = decision + + def build(self, forceTranscode=False): + if self.item.settings.getPreference("playback_directplay", True): + directPlayPref = self.item.settings.getPreference("playback_directplay_force", False) and 'forced' or 'allow' + else: + directPlayPref = 'disabled' + + if forceTranscode or directPlayPref == "disabled" or self.choice.hasBurnedInSubtitles is True: + directPlay = False + else: + directPlay = directPlayPref == "forced" and True or None + + return self._build(directPlay, self.item.settings.getPreference("playback_remux", True)) + + def _build(self, directPlay=None, directStream=True, currentPartIndex=None): + isForced = directPlay is not None + if isForced: + util.LOG(directPlay and "Forced Direct Play" or "Forced Transcode; allowDirectStream={0}".format(directStream)) + + directPlay = False if directPlay is False else self.choice.isDirectPlayable + + server = self.item.getServer() + + # A lot of our content metadata is independent of the direct play decision. + # Add that first. + + obj = util.AttributeDict() + obj.duration = self.media.duration.asInt() + + videoRes = self.media.getVideoResolution() + obj.fullHD = videoRes >= 1080 + obj.streamQualities = (videoRes >= 480 and self.item.settings.getGlobal("IsHD")) and ["HD"] or ["SD"] + + frameRate = self.media.videoFrameRate or "24p" + if frameRate == "24p": + obj.frameRate = 24 + elif frameRate == "NTSC": + obj.frameRate = 30 + + # Add soft subtitle info + if self.choice.subtitleDecision == self.choice.SUBTITLES_SOFT_ANY: + obj.subtitleUrl = server.buildUrl(self.choice.subtitleStream.getSubtitlePath(), True) + elif self.choice.subtitleDecision == self.choice.SUBTITLES_SOFT_DP: + obj.subtitleConfig = {'TrackName': "mkv/" + str(self.choice.subtitleStream.index.asInt() + 1)} + + # Create one content metadata object for each part and store them as a + # linked list. We probably want a doubly linked list, except that it + # becomes a circular reference nuisance, so we make the current item the + # base object and singly link in each direction from there. + + baseObj = obj + prevObj = None + startOffset = 0 + + startPartIndex = currentPartIndex or 0 + for partIndex in range(startPartIndex, len(self.media.parts)): + isCurrentPart = (currentPartIndex is not None and partIndex == currentPartIndex) + partObj = util.AttributeDict() + partObj.update(baseObj) + + partObj.live = False + partObj.partIndex = partIndex + partObj.startOffset = startOffset + + part = self.media.parts[partIndex] + + partObj.partDuration = part.duration.asInt() + partObj.path = str(part.file) + partObj.size = part.size and int(part.size) or '' + + if part.isIndexed(): + partObj.sdBifPath = part.getIndexPath("sd") + partObj.hdBifPath = part.getIndexPath("hd") + + # We have to evaluate every part before playback. Normally we'd expect + # all parts to be identical, but in reality they can be different. + + if partIndex > 0 and (not isForced and directPlay or not isCurrentPart): + choice = mediadecisionengine.MediaDecisionEngine().evaluateMediaVideo(self.item, self.media, partIndex) + canDirectPlay = (choice.isDirectPlayable is True) + else: + canDirectPlay = directPlay + + if canDirectPlay: + partObj = self.buildDirectPlay(partObj, partIndex) + else: + transcodeServer = self.item.getTranscodeServer(True, "video") + if transcodeServer is None: + return None + partObj = self.buildTranscode(transcodeServer, partObj, partIndex, directStream, isCurrentPart) + + # Set up our linked list references. If we couldn't build an actual + # object: fail fast. Otherwise, see if we're at our start offset + # yet in order to decide if we need to link forwards or backwards. + # We also need to account for parts missing a duration, by verifying + # the prevObj is None or if the startOffset has incremented. + + if partObj is None: + obj = None + break + elif prevObj is None or (startOffset > 0 and int(self.seekValue / 1000) >= startOffset): + obj = partObj + partObj.prevObj = prevObj + elif prevObj is not None: + prevObj.nextPart = partObj + + startOffset = startOffset + int(part.duration.asInt() / 1000) + + prevObj = partObj + + # Only set PlayStart for the initial part, and adjust for the part's offset + if obj is not None: + if obj.live: + # Start the stream at the end. Per Roku, this can be achieved using + # a number higher than the duration. Using the current time should + # ensure it's definitely high enough. + + obj.playStart = util.now() + 1800 + else: + obj.playStart = int(self.seekValue / 1000) - obj.startOffset + + self.metadata = obj + + util.LOG("Constructed video item for playback: {0}".format(dict(obj))) + + return self.metadata + + @property + def startOffset(self): + return self.metadata and self.metadata.startOffset or 0 + + def offsetIsValid(self, offset_seconds): + return self.metadata.startOffset <= offset_seconds < self.metadata.startOffset + (self.metadata.partDuration / 1000) + + def isLiveHls(url=None, headers=None): + # Check to see if this is a live HLS playlist to fix two issues. One is a + # Roku workaround since it doesn't obey the absence of EXT-X-ENDLIST to + # start playback at the END of the playlist. The second is for us to know + # if it's live to modify the functionality and player UI. + + # if IsString(url): + # request = createHttpRequest(url, "GET", true) + # AddRequestHeaders(request.request, headers) + # response = request.GetToStringWithTimeout(10) + + # ' Inspect one of the media playlist streams if this is a master playlist. + # if response.instr("EXT-X-STREAM-INF") > -1 then + # Info("Identify live HLS: inspecting the master playlist") + # mediaUrl = CreateObject("roRegex", "(^https?://.*$)", "m").Match(response)[1] + # if mediaUrl <> invalid then + # request = createHttpRequest(mediaUrl, "GET", true) + # AddRequestHeaders(request.request, headers) + # response = request.GetToStringWithTimeout(10) + # end if + # end if + + # isLiveHls = (response.Trim().Len() > 0 and response.instr("EXT-X-ENDLIST") = -1 and response.instr("EXT-X-STREAM-INF") = -1) + # Info("Identify live HLS: live=" + isLiveHls.toStr()) + # return isLiveHls + + return False + + def getServerDecision(self): + directPlay = not (self.metadata and self.metadata.isTranscoded) + decisionPath = self.getDecisionPath(directPlay) + newDecision = None + + if decisionPath: + server = self.metadata.transcodeServer or self.item.getServer() + request = plexrequest.PlexRequest(server, decisionPath) + response = request.getWithTimeout(10) + + if response.isSuccess() and response.container: + decision = serverdecision.ServerDecision(self, response, self) + + if decision.isSuccess(): + util.LOG("MDE: Server was happy with client's original decision. {0}".format(decision)) + return self + elif decision.isDecision(True): + util.WARN_LOG("MDE: Server was unhappy with client's original decision. {0}".format(decision)) + return decision.getDecision() + else: + util.LOG("MDE: Server was unbiased about the decision. {0}".format(decision)) + + # Check if the server has provided a new media item to use it. If + # there is no item, then we'll continue along as if there was no + # decision made. + newDecision = decision.getDecision(False) + else: + util.WARN_LOG("MDE: Server failed to provide a decision") + else: + util.WARN_LOG("MDE: Server or item does not support decisions") + + return newDecision or self + + def getDecisionPath(self, directPlay=False): + if not self.item or not self.metadata: + return None + + decisionPath = self.metadata.decisionPath + if not decisionPath: + server = self.metadata.transcodeServer or self.item.getServer() + decisionPath = self.buildTranscode(server, util.AttributeDict(), self.metadata.partIndex, True, False).decisionPath + + # Modify the decision params based on the transcode url + if decisionPath: + if directPlay: + decisionPath = decisionPath.replace("directPlay=0", "directPlay=1") + + # Clear all subtitle parameters and add the a valid subtitle type based + # on the video player. This will let the server decide if it can supply + # sidecar subs, burn or embed w/ an optional transcode. + for key in ("subtitles", "advancedSubtitles"): + decisionPath = re.sub('([?&]{0}=)\w+'.format(key), '', decisionPath) + subType = 'sidecar' # AppSettings().getBoolPreference("custom_video_player"), "embedded", "sidecar") + decisionPath = http.addUrlParam(decisionPath, "subtitles=" + subType) + + # Global variables for all decisions + # Kodi default is 20971520 (20MB) + decisionPath = http.addUrlParam(decisionPath, + "mediaBufferSize={}".format(str(CACHE_SIZE * 1024))) + decisionPath = http.addUrlParam(decisionPath, "hasMDE=1") + + if not advancedSettings.oldprofile: + decisionPath = http.addUrlParam(decisionPath, 'X-Plex-Client-Profile-Name=Generic') + else: + decisionPath = http.addUrlParam(decisionPath, 'X-Plex-Client-Profile-Name=Chrome') + + return decisionPath + + def getTranscodeReason(self): + # Combine the server and local MDE decisions + obj = [] + if self.decision: + obj.append(self.decision.getDecisionText()) + if self.item: + obj.append(self.item.transcodeReason) + reason = ' '.join(obj) + if not reason: + return None + + return reason + + def buildTranscodeHls(self, obj): + util.DEBUG_LOG('buildTranscodeHls()') + obj.streamFormat = "hls" + obj.streamBitrates = [0] + obj.switchingStrategy = "no-adaptation" + obj.transcodeEndpoint = "/video/:/transcode/universal/start.m3u8" + + builder = http.HttpRequest(obj.transcodeServer.buildUrl(obj.transcodeEndpoint, True)) + builder.extras = [] + builder.addParam("protocol", "hls") + + # TODO: This should be Generic, but will need to re-evaluate the augmentations with that change + if not advancedSettings.oldprofile: + builder.addParam("X-Plex-Client-Profile-Name", "Generic") + else: + builder.addParam("X-Plex-Client-Profile-Name", "Chrome") + + if self.choice.subtitleDecision == self.choice.SUBTITLES_SOFT_ANY: + builder.addParam("skipSubtitles", "1") + else: # elif self.choice.hasBurnedInSubtitles is True: # Must burn transcoded because we can't set offset + captionSize = captions.CAPTIONS.getBurnedSize() + if captionSize is not None: + builder.addParam("subtitleSize", captionSize) + + # Augment the server's profile for things that depend on the Roku's configuration. + if self.item.settings.supportsAudioStream("ac3", 6): + builder.extras.append("append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=hls&audioCodec=ac3)") + if not advancedSettings.oldprofile: + builder.extras.append("add-direct-play-profile(type=videoProfile&container=mkv&videoCodec=*&audioCodec=ac3)") + else: + builder.extras.append( + "add-direct-play-profile(type=videoProfile&container=matroska&videoCodec=*&audioCodec=ac3)") + + return builder + + def buildTranscodeMkv(self, obj, directStream=True): + util.DEBUG_LOG('buildTranscodeMkv()') + obj.streamFormat = "mkv" + obj.streamBitrates = [0] + obj.transcodeEndpoint = "/video/:/transcode/universal/start.mkv" + + builder = http.HttpRequest(obj.transcodeServer.buildUrl(obj.transcodeEndpoint, True)) + builder.extras = [] + builder.addParam("protocol", "http") + builder.addParam("copyts", "1") + if not advancedSettings.oldprofile: + builder.addParam("X-Plex-Client-Profile-Name", "Generic") + else: + builder.addParam("X-Plex-Client-Profile-Name", "Chrome") + + obj.subtitleUrl = None + + clampToOrig = self.item.settings.getPreference("audio_clamp_to_orig", True) + useKodiAudio = self.item.settings.getPreference("audio_channels_kodi", False) + AC3Cond = self.item.settings.getPreference("audio_force_ac3_cond", 'never') + dtsIsAC3 = self.item.settings.getPreference("audio_ac3dts", True) + hasAudioChoice = self.choice.audioStream is not None + forceAC3 = AC3Cond != 'never' + + ach = None + if AC3Cond in ('2', '5'): + ach = int(AC3Cond) + + # fixme: still necessary? + if self.choice.subtitleDecision == self.choice.SUBTITLES_BURN: + builder.addParam("subtitles", "burn") + captionSize = captions.CAPTIONS.getBurnedSize() + if captionSize is not None: + builder.addParam("subtitleSize", captionSize) + + else: + # TODO(rob): can we safely assume the id will also be 3 (one based index). + # If not, we will have to get tricky and select the subtitle stream after + # video playback starts via roCaptionRenderer: GetSubtitleTracks() and + # ChangeSubtitleTrack() + + obj.subtitleConfig = {'TrackName': "mkv/3" if hasAudioChoice else "mkv/2"} + + # Allow text conversion of subtitles if we only burn image formats + #if self.item.settings.getPreference("burn_subtitles") == "image": + if not self.item.settings.getPreference("burn_ssa", True): + builder.addParam("advancedSubtitles", "text") + + builder.addParam("subtitles", "auto") + + if not forceAC3: + if directStream: + audioCodecs = "eac3,ac3,dca,aac,mp3,mp2,pcm,flac,alac,wmav2,wmapro,wmavoice,opus,vorbis,truehd" + else: + audioCodecs = "mp3,ac3,dca,aac,opus" + else: + if dtsIsAC3: + audioCodecs = "ac3,dca" + else: + audioCodecs = "ac3" + + subtitleCodecs = "srt,ssa,ass,mov_text,tx3g,ttxt,text,pgs,vobsub,smi,subrip,eia_608_embedded," \ + "eia_708_embedded,dvb_subtitle" + (",webvtt" if KODI_VERSION_MAJOR > 19 else '') + + + util.LOG('MDE-prep: enabling codecs: {}'.format(audioCodecs)) + + # Allow virtually anything in Kodi playback. + + # DP might not do anything here + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=" + # "h264,mpeg1video,mpeg2video,mpeg4,msmpeg4v2,msmpeg4v3,vc1,wmv3&container=*&" + # "audioCodec="+audioCodecs+"&protocol=http)") + + builder.extras.append( + "add-transcode-target(type=videoProfile&videoCodec=" + "h264,mpeg1video,mpeg2video,mpeg4,msmpeg4v2,msmpeg4v3,vc1,wmv3&container=mkv&" + "audioCodec={}&subtitleCodec={}&protocol=http&context=streaming)".format(audioCodecs, subtitleCodecs)) + + # builder.extras.append( + # "append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=http&audioCodec=" + + # audioCodecs + ")") + + # if self.item.settings.supportsSurroundSound(): + # if self.choice.audioStream is not None: + # numChannels = self.choice.audioStream.channels.asInt(8) + # else: + # numChannels = 8 + # + # for codec in ("ac3", "eac3", "dca"): + # if self.item.settings.supportsAudioStream(codec, numChannels): + # builder.extras.append("append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=http&audioCodec=" + codec + ")") + # builder.extras.append("add-direct-play-profile(type=videoProfile&videoCodec=*&container=mkv&audioCodec=" + codec + ")") + # if codec == "dca": + # builder.extras.append( + # "add-limitation(scope=videoAudioCodec&scopeName=dca&type=upperBound&name=audio.channels&value=8&isRequired=false)" + # ) + # + # for codec in ("ac3", "eac3", "dca"): + # builder.extras.append("append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=http&audioCodec=" + codec + ")") + # builder.extras.append("add-direct-play-profile(type=videoProfile&videoCodec=*&container=mkv&audioCodec=" + codec + ")") + + util.LOG('MDE-prep: settings: clampOrig: {}, kodiAudio: {}, forceAC3: {}, dtsIsAC3: {}' + .format(clampToOrig, useKodiAudio, forceAC3, dtsIsAC3)) + + # limit audio channels to original stream's audio channel amount + numChannels = self.choice.audioStream.channels.asInt(8) if hasAudioChoice and \ + self.choice.audioStream.channels else 8 + + # limit OPUS to 334kbit + if numChannels == 8: + # 7.1 + opusBitrate = 334 + elif numChannels >= 6: + # 5.1 + opusBitrate = 256 + else: + # 2 + opusBitrate = 128 + + # limit max audio channels to audio stream or kodi (whichever is lower) + maxAudioChannels = numChannels if not useKodiAudio else min(numChannels, self.audioChannels) + + # if we've got a channel limit for AC3/DTS, apply it + maxAudioChannels = maxAudioChannels if not ach else min(maxAudioChannels, ach) + + if forceAC3 and hasAudioChoice: + # limit max audio channels to the above or 6 for AC3 (whichever is lower) + if self.choice.audioStream.codec != "dca": + maxAudioChannels = min(6, maxAudioChannels) + else: + # allow DTS 6.1 ES + maxAudioChannels = min(7, maxAudioChannels) + + streamWasAC3 = hasAudioChoice and self.choice.audioStream.codec == "ac3" + + if not forceAC3 and hasAudioChoice: + # limit audio bitrate to the same bitrate as the current stream's codec + if clampToOrig and self.choice.audioStream.bitrate: + util.LOG('MDE-prep: limiting {} to {} kbit'.format(self.choice.audioStream.codec.upper(), + self.choice.audioStream.bitrate)) + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName={}&" + "type=upperBound&name=audio.bitrate&value={})".format( + self.choice.audioStream.codec, + self.choice.audioStream.bitrate + ) + ) + + # limit OPUS bitrate + if hasAudioChoice and self.choice.audioStream.codec != "opus": + util.LOG('MDE-prep: limiting OPUS bitrate to {} kbit'.format(opusBitrate)) + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=opus&type=upperBound&name=audio.bitrate&" + "value={}&isRequired=false)".format(opusBitrate) + ) + + # limit AC3 + if not streamWasAC3 or forceAC3: + util.LOG('MDE-prep: limiting AC3 to 640 kbit') + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=ac3&type=upperBound&name=audio.bitrate&value=640)" + ) + + util.LOG('MDE-prep: limiting audio channels to {}'.format(maxAudioChannels)) + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=*&type=upperBound&" + "name=audio.channels&value={})".format(maxAudioChannels) + ) + + # AAC sample rate cannot be less than 22050hz (HLS is capable). + if self.choice.audioStream is not None and self.choice.audioStream.samplingRate.asInt(22050) < 22050: + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=aac&type=lowerBound&" + "name=audio.samplingRate&value=22050&isRequired=false)") + + # HEVC + if self.item.settings.getPreference("allow_hevc", True): + builder.extras.append( + "append-transcode-target-codec(type=videoProfile&context=streaming&container=mkv&" + "protocol=http&videoCodec=hevc)") + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=hevc&container=*&audioCodec=*)") + + # VP9 + if self.item.settings.getGlobal("vp9Support"): + builder.extras.append( + "append-transcode-target-codec(type=videoProfile&context=streaming&container=mkv&" + "protocol=http&videoCodec=vp9)") + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=vp9&container=*&audioCodec=*)") + + # AV1 + if self.item.settings.getPreference("allow_av1", False): + builder.extras.append( + "append-transcode-target-codec(type=videoProfile&context=streaming&container=mkv&" + "protocol=http&videoCodec=av1)") + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=av1&container=*&audioCodec=*)") + + return builder + + def buildTranscodeMkvLegacy(self, obj, directStream=True): + util.DEBUG_LOG('buildTranscodeMkvLegacy()') + obj.streamFormat = "mkv" + obj.streamBitrates = [0] + obj.transcodeEndpoint = "/video/:/transcode/universal/start.mkv" + + builder = http.HttpRequest(obj.transcodeServer.buildUrl(obj.transcodeEndpoint, True)) + builder.extras = [] + builder.addParam("protocol", "http") + builder.addParam("copyts", "1") + builder.addParam("X-Plex-Client-Profile-Name", "Generic") + + obj.subtitleUrl = None + + # fixme: still necessary? + if True: # if self.choice.subtitleDecision == self.choice.SUBTITLES_BURN: # Must burn transcoded because we can't set offset + builder.addParam("subtitles", "burn") + captionSize = captions.CAPTIONS.getBurnedSize() + if captionSize is not None: + builder.addParam("subtitleSize", captionSize) + + else: + # TODO(rob): can we safely assume the id will also be 3 (one based index). + # If not, we will have to get tricky and select the subtitle stream after + # video playback starts via roCaptionRenderer: GetSubtitleTracks() and + # ChangeSubtitleTrack() + + obj.subtitleConfig = {'TrackName': "mkv/3"} + + # Allow text conversion of subtitles if we only burn image formats + if self.item.settings.getPreference("burn_subtitles") == "image": + builder.addParam("advancedSubtitles", "text") + + builder.addParam("subtitles", "auto") + + if directStream: + audioCodecs = "eac3,ac3,dca,aac,mp3,mp2,pcm,flac,alac,wmav2,wmapro,wmavoice,opus,vorbis,truehd" + else: + audioCodecs = "mp3,ac3,aac,opus" + + # Allow virtually anything in Kodi playback. + + # DP might not do anything here + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=" + # "h264,mpeg1video,mpeg2video,mpeg4,msmpeg4v2,msmpeg4v3,vc1,wmv3&container=*&" + # "audioCodec="+audioCodecs+"&protocol=http)") + + builder.extras.append( + "add-transcode-target(type=videoProfile&videoCodec=" + "h264,mpeg1video,mpeg2video,mpeg4,msmpeg4v2,msmpeg4v3,vc1,wmv3&container=mkv&" + "audioCodec="+audioCodecs+"&protocol=http&context=streaming)") + + # builder.extras.append( + # "append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=http&audioCodec=" + + # audioCodecs + ")") + + # if self.item.settings.supportsSurroundSound(): + # if self.choice.audioStream is not None: + # numChannels = self.choice.audioStream.channels.asInt(8) + # else: + # numChannels = 8 + # + # for codec in ("ac3", "eac3", "dca"): + # if self.item.settings.supportsAudioStream(codec, numChannels): + # builder.extras.append("append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=http&audioCodec=" + codec + ")") + # builder.extras.append("add-direct-play-profile(type=videoProfile&videoCodec=*&container=mkv&audioCodec=" + codec + ")") + # if codec == "dca": + # builder.extras.append( + # "add-limitation(scope=videoAudioCodec&scopeName=dca&type=upperBound&name=audio.channels&value=8&isRequired=false)" + # ) + # + # for codec in ("ac3", "eac3", "dca"): + # builder.extras.append("append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=http&audioCodec=" + codec + ")") + # builder.extras.append("add-direct-play-profile(type=videoProfile&videoCodec=*&container=mkv&audioCodec=" + codec + ")") + + # limit OPUS to 334kbit + numChannels = self.choice.audioStream.channels.asInt(8) if self.choice.audioStream else 8 + + if numChannels == 8: + # 7.1 + opusBitrate = 334 + elif numChannels >= 6: + # 5.1 + opusBitrate = 256 + else: + # 2 + opusBitrate = 128 + + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=opus&type=upperBound&name=audio.bitrate&" + "value={}&isRequired=false)".format(opusBitrate) + ) + + # limit AC3 + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=ac3&type=upperBound&name=audio.bitrate&value=640)" + ) + + # limit audio to Kodi audio channels + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=*&type=upperBound&" + "name=audio.channels&value={})".format(self.audioChannels) + ) + + # AAC sample rate cannot be less than 22050hz (HLS is capable). + if self.choice.audioStream is not None and self.choice.audioStream.samplingRate.asInt(22050) < 22050: + builder.extras.append( + "add-limitation(scope=videoAudioCodec&scopeName=aac&type=lowerBound&" + "name=audio.samplingRate&value=22050&isRequired=false)") + + # HEVC + if self.item.settings.getPreference("allow_hevc", True): + builder.extras.append( + "append-transcode-target-codec(type=videoProfile&context=streaming&container=mkv&" + "protocol=http&videoCodec=hevc)") + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=hevc&container=*&audioCodec=*)") + + # VP9 + if self.item.settings.getGlobal("vp9Support"): + builder.extras.append( + "append-transcode-target-codec(type=videoProfile&context=streaming&container=mkv&" + "protocol=http&videoCodec=vp9)") + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=vp9&container=*&audioCodec=*)") + + # AV1 + if self.item.settings.getPreference("allow_av1", False): + builder.extras.append( + "append-transcode-target-codec(type=videoProfile&context=streaming&container=mkv&" + "protocol=http&videoCodec=av1)") + # builder.extras.append( + # "add-direct-play-profile(type=videoProfile&videoCodec=av1&container=*&audioCodec=*)") + + return builder + + def buildDirectPlay(self, obj, partIndex): + util.DEBUG_LOG('buildDirectPlay()') + part = self.media.parts[partIndex] + + server = self.item.getServer() + + # Check if we should include our token or not for this request + obj.isRequestToServer = server.isRequestToServer(server.buildUrl(part.getAbsolutePath("key"))) + obj.streamUrls = [server.buildUrl(part.getAbsolutePath("key"), obj.isRequestToServer)] + obj.token = obj.isRequestToServer and server.getToken() or None + if self.media.protocol == "hls": + obj.streamFormat = "hls" + obj.switchingStrategy = "full-adaptation" + obj.live = self.isLiveHLS(obj.streamUrls[0], self.media.indirectHeaders) + else: + obj.streamFormat = self.media.get('container', 'mp4') + if obj.streamFormat == "mov" or obj.streamFormat == "m4v": + obj.streamFormat = "mp4" + + obj.streamBitrates = [self.media.bitrate.asInt()] + obj.isTranscoded = False + + if self.choice.audioStream is not None: + obj.audioLanguageSelected = self.choice.audioStream.languageCode + + return obj + + def hasMoreParts(self): + return (self.metadata is not None and self.metadata.nextPart is not None) + + def getNextPartOffset(self): + return self.metadata.nextPart.startOffset * 1000 + + def goToNextPart(self): + oldPart = self.metadata + if oldPart is None: + return + + newPart = oldPart.nextPart + if newPart is None: + return + + newPart.prevPart = oldPart + oldPart.nextPart = None + self.metadata = newPart + + util.LOG("Next part set for playback: {0}".format(self.metadata)) + + def getBifUrl(self, offset=0): + server = self.item.getServer() + startOffset = 0 + for part in self.media.parts: + duration = part.duration.asInt() + if startOffset <= offset < startOffset + duration: + bifUrl = part.getIndexPath("hd") or part.getIndexPath("sd") + if bifUrl is not None: + url = server.buildUrl('{0}/{1}'.format(bifUrl, offset - startOffset), True) + return url + + startOffset += duration + + return None + + def buildTranscode(self, server, obj, partIndex, directStream, isCurrentPart): + util.DEBUG_LOG('buildTranscode()') + obj.transcodeServer = server + obj.isTranscoded = True + + # if server.supportsFeature("mkvTranscode") and self.item.settings.getPreference("transcode_format", 'mkv') != "hls": + if server.supportsFeature("mkvTranscode"): + if not advancedSettings.oldprofile: + builder = self.buildTranscodeMkv(obj, directStream=directStream) + else: + builder = self.buildTranscodeMkvLegacy(obj, directStream=directStream) + else: + builder = self.buildTranscodeHls(obj) + + if self.item.getServer().TYPE == 'MYPLEXSERVER': + path = server.swizzleUrl(self.item.getAbsolutePath("key")) + else: + path = self.item.getAbsolutePath("key") + + builder.addParam("path", path) + + part = self.media.parts[partIndex] + seekOffset = int(self.seekValue / 1000) + startOffset = obj.get("startOffset", 0) + + # Disabled for HLS due to a Roku bug plexinc/roku-client-issues#776 + if True: # obj.streamFormat == "mkv": + # Trust our seekOffset for this part if it's the current part (now playing) or + # the seekOffset is within the time frame. We have to trust the current part + # as we may have to rebuild the transcode when seeking, and not all parts + # have a valid duration. + + if isCurrentPart or len(self.media.parts) <= 1 or ( + seekOffset >= startOffset and seekOffset <= startOffset + int(part.duration.asInt() / 1000) + ): + startOffset = seekOffset - startOffset + + # Avoid a perfect storm of PMS and Roku quirks. If we pass an offset to + # the transcoder,: it'll start transcoding from that point. But if + # we try to start a few seconds into the video, the Roku seems to want + # to grab the first segment. The first segment doesn't exist, so PMS + # returns a 404 (but only if the offset is <= 12s, otherwise it returns + # a blank segment). If the Roku gets a 404 for the first segment,: + # it'll fail. So, if we're going to start playing from less than 12 + # seconds, don't bother telling the transcoder. It's not worth the + # potential failure, let it transcode from the start so that the first + # segment will always exist. + + # TODO: Probably can remove this (Rick) + if startOffset <= 12: + startOffset = 0 + else: + startOffset = 0 + + builder.addParam("offset", str(startOffset)) + + builder.addParam("session", self.item.settings.getGlobal("clientIdentifier")) + builder.addParam("directStream", directStream and "1" or "0") + builder.addParam("directPlay", "0") + + qualityIndex = self.item.settings.getQualityIndex(self.item.getQualityType(server)) + builder.addParam("videoQuality", self.item.settings.getGlobal("transcodeVideoQualities")[qualityIndex]) + builder.addParam("videoResolution", str(self.item.settings.getGlobal("transcodeVideoResolutions")[qualityIndex])) + builder.addParam("maxVideoBitrate", self.item.settings.getGlobal("transcodeVideoBitrates")[qualityIndex]) + + if self.media.mediaIndex is not None: + builder.addParam("mediaIndex", str(self.media.mediaIndex)) + + builder.addParam("partIndex", str(partIndex)) + + # Augment the server's profile for things that depend on the Roku's configuration. + if self.item.settings.getPreference("h264_level", "auto") != "auto": + builder.extras.append( + "add-limitation(scope=videoCodec&scopeName=h264&type=upperBound&name=video.level&value={0}&isRequired=true)".format( + self.item.settings.getPreference("h264_level") + ) + ) + + if not self.item.settings.getGlobal("supports1080p60") and self.item.settings.getGlobal("transcodeVideoResolutions")[qualityIndex][0] >= 1920: + builder.extras.append("add-limitation(scope=videoCodec&scopeName=h264&type=upperBound&name=video.frameRate&value=30&isRequired=false)") + + if builder.extras: + builder.addParam("X-Plex-Client-Profile-Extra", '+'.join(builder.extras)) + + if server.isLocalConnection(): + builder.addParam("location", "lan") + + obj.streamUrls = [builder.getUrl()] + + # Build the decision path now that we have build our stream url, and only if the server supports it. + if server.supportsFeature("streamingBrain"): + decisionPath = builder.getRelativeUrl().replace(obj.transcodeEndpoint, self.DECISION_ENDPOINT) + if decisionPath.startswith(self.DECISION_ENDPOINT): + obj.decisionPath = decisionPath + + return obj + + +class PlexAudioPlayer(object): + def __init__(self, item): + self.containerFormats = { + 'aac': "es.aac-adts" + } + + self.item = item + self.choice = mediadecisionengine.MediaDecisionEngine().chooseMedia(item) + if self.choice: + self.media = self.choice.media + self.lyrics = None # createLyrics(item, self.media) + + def build(self, directPlay=None): + directPlay = directPlay or self.choice.isDirectPlayable + + obj = util.AttributeDict() + + # TODO(schuyler): Do we want/need to add anything generic here? Title? Duration? + + if directPlay: + obj = self.buildDirectPlay(obj) + else: + obj = self.buildTranscode(obj) + + self.metadata = obj + + util.LOG("Constructed audio item for playback: {0}".format(dict(obj))) + + return self.metadata + + def buildTranscode(self, obj): + transcodeServer = self.item.getTranscodeServer(True, "audio") + if not transcodeServer: + return None + + obj.streamFormat = "mp3" + obj.isTranscoded = True + obj.transcodeServer = transcodeServer + obj.transcodeEndpoint = "/music/:/transcode/universal/start.m3u8" + + builder = http.HttpRequest(transcodeServer.buildUrl(obj.transcodeEndpoint, True)) + # builder.addParam("protocol", "http") + builder.addParam("path", self.item.getAbsolutePath("key")) + builder.addParam("session", self.item.getGlobal("clientIdentifier")) + builder.addParam("directPlay", "0") + builder.addParam("directStream", "0") + + obj.url = builder.getUrl() + + return obj + + def buildDirectPlay(self, obj): + if self.choice.part: + obj.url = self.item.getServer().buildUrl(self.choice.part.getAbsolutePath("key"), True) + + # Set and override the stream format if applicable + obj.streamFormat = self.choice.media.get('container', 'mp3') + if self.containerFormats.get(obj.streamFormat): + obj.streamFormat = self.containerFormats[obj.streamFormat] + + # If we're direct playing a FLAC, bitrate can be required, and supposedly + # this is the only way to do it. plexinc/roku-client#48 + # + bitrate = self.choice.media.bitrate.asInt() + if bitrate > 0: + obj.streams = [{'url': obj.url, 'bitrate': bitrate}] + + return obj + + # We may as well fallback to transcoding if we could not direct play + return self.buildTranscode(obj) + + def getLyrics(self): + return self.lyrics + + def hasLyrics(self): + return False + return self.lyrics.isAvailable() + + +class PlexPhotoPlayer(object): + def __init__(self, item): + self.item = item + self.choice = item + self.media = item.media()[0] + self.metadata = None + + def build(self, item=None): + item = item or self.item + media = item.media()[0] + if media.parts and media.parts[0]: + obj = util.AttributeDict() + + part = media.parts[0] + path = part.key or part.thumb + server = item.getServer() + + obj.url = server.buildUrl(path, True) + obj.enableBlur = server.supportsPhotoTranscoding + + util.DEBUG_LOG("Constructed photo item for playback: {0}".format(dict(obj))) + + self.metadata = obj + + return self.metadata diff --git a/script.plexmod/lib/_included_packages/plexnet/plexrequest.py b/script.plexmod/lib/_included_packages/plexnet/plexrequest.py new file mode 100644 index 000000000..764ae1d58 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexrequest.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import +from xml.etree import ElementTree + +from . import plexserver +from . import plexresult +from . import http +from . import util + + +class PlexRequest(http.HttpRequest): + def __init__(self, server, path, method=None): + server = server or plexserver.dummyPlexServer() + + http.HttpRequest.__init__(self, server.buildUrl(path, includeToken=True), method) + + self.server = server + self.path = path + + util.addPlexHeaders(self, server.getToken()) + + def onResponse(self, event, context): + if context.get('completionCallback'): + result = plexresult.PlexResult(self.server, self.path) + result.setResponse(event) + context['completionCallback'](self, result, context) + + def doRequestWithTimeout(self, timeout=10, postBody=None): + # non async request/response + if postBody: + data = ElementTree.fromstring(self.postToStringWithTimeout(postBody, timeout)) + else: + data = ElementTree.fromstring(self.getToStringWithTimeout(timeout)) + + response = plexresult.PlexResult(self.server, self.path) + response.setResponse(self.event) + response.parseFakeXMLResponse(data) + + return response + + +class PlexServerRequest(PlexRequest): + def onResponse(self, event, context): + if context.get('completionCallback'): + result = plexresult.PlexServerResult(self.server, self.path) + result.setResponse(event) + context['completionCallback'](self, result, context) diff --git a/script.plexmod/lib/_included_packages/plexnet/plexresource.py b/script.plexmod/lib/_included_packages/plexnet/plexresource.py new file mode 100644 index 000000000..11dd75f95 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexresource.py @@ -0,0 +1,231 @@ +from __future__ import absolute_import +from xml.etree import ElementTree + +from . import http +from . import exceptions +from . import plexobjects +from . import plexconnection +from . import util + +RESOURCES = 'https://plex.tv/api/resources?includeHttps=1' + + +class PlexResource(object): + def __init__(self, data): + self.connection = None + self.connections = [] + self.accessToken = None + self.sourceType = None + + if data is None: + return + + self.accessToken = data.attrib.get('accessToken') + self.httpsRequired = data.attrib.get('httpsRequired') == '1' + self.type = data.attrib.get('type') + self.clientIdentifier = data.attrib.get('clientIdentifier') + self.product = data.attrib.get('product') + self.provides = data.attrib.get('provides') + self.serverClass = data.attrib.get('serverClass') + self.sourceType = data.attrib.get('sourceType') + self.uuid = self.clientIdentifier + + hasSecureConn = False + + for conn in data.findall('Connection'): + if conn.attrib.get('protocol') == "https": + hasSecureConn = True + break + + addLocalConsFound = [] + for conn in data.findall('Connection'): + connection = plexconnection.PlexConnection( + plexconnection.PlexConnection.SOURCE_MYPLEX, + conn.attrib.get('uri'), + conn.attrib.get('local') == '1', + self.accessToken, + hasSecureConn and conn.attrib.get('protocol') != "https" + ) + + # Keep the secure connection on top + if connection.isSecure and not util.LOCAL_OVER_SECURE: + self.connections.insert(0, connection) + elif not connection.isSecure and util.LOCAL_OVER_SECURE: + self.connections.insert(0, connection) + else: + self.connections.append(connection) + + if connection.isSecureButLocal: + addLocalConsFound.append((connection.isSecureButLocal, connection.address)) + + # If the connection is one of our plex.direct secure connections, add + # the nonsecure variant as well, unless https is required. + # + if self.httpsRequired and conn.attrib.get('protocol') == "https" and conn.attrib.get('address') not in conn.attrib.get('uri'): + self.connections.append( + plexconnection.PlexConnection( + plexconnection.PlexConnection.SOURCE_MYPLEX, + "http://{0}:{1}".format(conn.attrib.get('address'), conn.attrib.get('port')), + conn.attrib.get('local') == '1', + self.accessToken, + True + ) + ) + + # add discovered local cons if necessary + for ipPort, origAddress in addLocalConsFound: + ip, port = ipPort + address = "http://" + ip + ":" + str(port) + for conn in self.connections: + if conn.address == address: + continue + + util.DEBUG_LOG( + "Secure connection {0} has a locally reachable IP, add it to the checklist".format(origAddress)) + lcon = plexconnection.PlexConnection( + plexconnection.PlexConnection.SOURCE_DISCOVERED, + "http://" + ip + ":" + str(port), + True, + self.accessToken, + not util.LOCAL_OVER_SECURE, + skipLocalCheck=True + ) + if util.LOCAL_OVER_SECURE: + self.connections.insert(0, lcon) + else: + self.connections.append(lcon) + + def __repr__(self): + return '<{0}:{1}>'.format(self.__class__.__name__, self.name.encode('utf8')) + + +class ResourceConnection(plexobjects.PlexObject): + # Constants + STATE_UNKNOWN = "unknown" + STATE_UNREACHABLE = "unreachable" + STATE_REACHABLE = "reachable" + STATE_UNAUTHORIZED = "unauthorized" + STATE_INSECURE = "insecure_untested" + + SOURCE_MANUAL = 1 + SOURCE_DISCOVERED = 2 + SOURCE_MYPLEX = 4 + + SCORE_REACHABLE = 4 + SCORE_LOCAL = 2 + SCORE_SECURE = 1 + + def init(self, data): + self.secure = True + self.reachable = False + self.data = None + + def __repr__(self): + return '<{0}:{1}>'.format(self.__class__.__name__, self.uri.encode('utf8')) + + @property + def http_uri(self): + return 'http://{0}:{1}'.format(self.address, self.port) + + @property + def URL(self): + if self.secure: + return self.uri + else: + return self.http_url + + def connect(self): + util.LOG('Connecting: {0}'.format(util.cleanToken(self.URL))) + try: + self.data = self.query('/') + self.reachable = True + return True + except Exception as err: + util.ERROR(util.cleanToken(self.URL), err) + + util.LOG('Connecting: Secure failed, trying insecure...') + self.secure = False + + try: + self.data = self.query('/') + self.reachable = True + return True + except Exception as err: + util.ERROR(util.cleanToken(self.URL), err) + + return False + + def headers(self, token=None): + headers = util.BASE_HEADERS.copy() + if token: + headers['X-Plex-Token'] = token + return headers + + def query(self, path, method=None, token=None, **kwargs): + method = method or http.requests.get + url = self.getURL(path) + util.LOG('{0} {1}'.format(method.__name__.upper(), url)) + response = method(url, headers=self.headers(token), timeout=util.TIMEOUT, **kwargs) + if response.status_code not in (200, 201): + codename = http.status_codes.get(response.status_code)[0] + raise exceptions.BadRequest('({0}) {1}'.format(response.status_code, codename)) + data = response.text.encode('utf8') + + return ElementTree.fromstring(data) if data else None + + def getURL(self, path, token=None): + if token: + delim = '&' if '?' in path else '?' + return '{base}{path}{delim}X-Plex-Token={token}'.format(base=self.URL, path=path, delim=delim, token=util.hideToken(token)) + return '{0}{1}'.format(self.URL, path) + + +class PlexResourceList(plexobjects.PlexItemList): + def __init__(self, data, initpath=None, server=None): + self._data = data + self.initpath = initpath + self._server = server + self._items = None + + @property + def items(self): + if self._items is None: + if self._data is not None: + self._items = [PlexResource(elem, initpath=self.initpath, server=self._server) for elem in self._data] + else: + self._items = [] + + return self._items + + +def fetchResources(token): + headers = util.BASE_HEADERS.copy() + headers['X-Plex-Token'] = token + util.LOG('GET {0}?X-Plex-Token={1}'.format(RESOURCES, util.hideToken(token))) + response = http.GET(RESOURCES) + data = ElementTree.fromstring(response.text.encode('utf8')) + from . import plexserver + return [plexserver.PlexServer(elem) for elem in data] + + +def findResource(resources, search, port=32400): + """ Searches server.name """ + search = search.lower() + util.LOG('Looking for server: {0}'.format(search)) + for server in resources: + if search == server.name.lower(): + util.LOG('Server found: {0}'.format(server)) + return server + util.LOG('Unable to find server: {0}'.format(search)) + raise exceptions.NotFound('Unable to find server: {0}'.format(search)) + + +def findResourceByID(resources, ID): + """ Searches server.clientIdentifier """ + util.LOG('Looking for server by ID: {0}'.format(ID)) + for server in resources: + if ID == server.clientIdentifier: + util.LOG('Server found by ID: {0}'.format(server)) + return server + util.LOG('Unable to find server by ID: {0}'.format(ID)) + raise exceptions.NotFound('Unable to find server by ID: {0}'.format(ID)) diff --git a/script.plexmod/lib/_included_packages/plexnet/plexresult.py b/script.plexmod/lib/_included_packages/plexnet/plexresult.py new file mode 100644 index 000000000..7d65d4f4b --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexresult.py @@ -0,0 +1,102 @@ +from __future__ import absolute_import +from . import http +from . import plexobjects + + +class PlexResult(http.HttpResponse): + def __init__(self, server, address): + self.server = server + self.address = address + self.container = None + self.parsed = None + self.items = [] + + def setResponse(self, event): + self.event = event + + def parseResponse(self): + if self.parsed: + return self.parsed + + self.parsed = False + + if self.isSuccess(): + data = self.getBodyXml() + if data is not None: + self.container = plexobjects.PlexContainer(data, initpath=self.address, server=self.server, address=self.address) + + for node in data: + self.addItem(self.container, node) + + self.parsed = True + + return self.parsed + + def parseFakeXMLResponse(self, data): + if self.parsed: + return self.parsed + + self.parsed = False + + if data is not None: + self.container = plexobjects.PlexContainer(data, initpath=self.address, server=self.server, address=self.address) + + for node in data: + self.addItem(self.container, node) + + self.parsed = True + + return self.parsed + + def addItem(self, container, node): + if node.attrib.get('type') in ('track', 'movie', 'episode', 'photo') and node.tag != 'PlayQueue': + item = plexobjects.buildItem(self.server, node, self.address, container=self.container) + else: + item = plexobjects.PlexObject(node, server=self.container.server, container=self.container) + + # TODO(rob): handle channel settings. We should be able to utilize + # the settings component with some modifications. + if not item.isSettings(): + self.items.append(item) + else: + # Decrement the size and total size if applicable + if self.container.get("size"): + self.container.size = plexobjects.PlexValue(str(self.container.size.asInt() - 1)) + if self.container.get("totalSize"): + self.container.totalSize = plexobjects.PlexValue(str(self.container.totalSize.asInt() - 1)) + + +class PlexServerResult(PlexResult): + def parseResponse(self): + if self.parsed: + return self.parsed + + self.parsed = False + + if self.isSuccess(): + data = self.getBodyXml() + if data is not None: + self.container = plexobjects.PlexServerContainer(data, initpath=self.address, server=self.server, address=self.address) + + for node in data: + self.addItem(self.container, node) + + self.parsed = True + + return self.parsed + + def parseFakeXMLResponse(self, data): + if self.parsed: + return self.parsed + + self.parsed = False + + if data is not None: + self.container = plexobjects.PlexServerContainer(data, initpath=self.address, server=self.server, address=self.address) + + for node in data: + self.addItem(self.container, node) + + self.parsed = True + + return self.parsed diff --git a/script.plexmod/lib/_included_packages/plexnet/plexserver.py b/script.plexmod/lib/_included_packages/plexnet/plexserver.py new file mode 100644 index 000000000..9bb4b6cd5 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexserver.py @@ -0,0 +1,700 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import time +import re +import json +import urllib3.exceptions + +from . import http +from . import util +from . import exceptions +from . import compat +from . import verlib + +from xml.etree import ElementTree +from . import signalsmixin +from . import plexobjects +from . import plexresource +from . import plexlibrary +from . import asyncadapter +from six.moves import range +# from plexapi.client import Client +# from plexapi.playqueue import PlayQueue + + +TOTAL_QUERIES = 0 +DEFAULT_BASEURI = 'http://localhost:32400' + + +class PlexServer(plexresource.PlexResource, signalsmixin.SignalsMixin): + TYPE = 'PLEXSERVER' + + def __init__(self, data=None): + signalsmixin.SignalsMixin.__init__(self) + plexresource.PlexResource.__init__(self, data) + self.accessToken = None + self.multiuser = False + self.isSupported = None + self.hasFallback = False + self.supportsAudioTranscoding = False + self.supportsVideoTranscoding = False + self.supportsPhotoTranscoding = False + self.supportsVideoRemuxOnly = False + self.supportsScrobble = True + self.allowsMediaDeletion = False + self.allowChannelAccess = False + self.activeConnection = None + self.serverClass = None + + self.pendingReachabilityRequests = 0 + self.pendingSecureRequests = 0 + + self.features = {} + self.librariesByUuid = {} + + self.server = self + self.session = http.Session() + + self.owner = None + self.owned = False + self.synced = False + self.sameNetwork = False + self.uuid = None + self.name = None + self.platform = None + self.versionNorm = None + self.rawVersion = None + self.transcodeSupport = False + + if data is None: + return + + self.owner = data.attrib.get('sourceTitle') + self.owned = data.attrib.get('owned') == '1' + self.synced = data.attrib.get('synced') == '1' + self.sameNetwork = data.attrib.get('publicAddressMatches') == '1' + self.uuid = data.attrib.get('clientIdentifier') + self.name = data.attrib.get('name') + self.platform = data.attrib.get('platform') + self.rawVersion = data.attrib.get('productVersion') + self.versionNorm = util.normalizedVersion(self.rawVersion) + self.transcodeSupport = data.attrib.get('transcodeSupport') == '1' + + def __eq__(self, other): + if not other: + return False + if self.__class__ != other.__class__: + + return False + return self.uuid == other.uuid and self.owner == other.owner + + def __ne__(self, other): + return not self.__eq__(other) + + def __str__(self): + return ""\ + .format(repr(self.name), self.owned, self.uuid, self.versionNorm, self.activeConnection) + + def __repr__(self): + return self.__str__() + + def close(self): + self.session.cancel() + + def get(self, attr, default=None): + return default + + @property + def isSecure(self): + if self.activeConnection: + return self.activeConnection.isSecure + + @property + def isLocal(self): + if self.activeConnection: + return self.activeConnection.isLocal + + def getObject(self, key): + data = self.query(key) + return plexobjects.buildItem(self, data[0], key, container=self) + + def hubs(self, section=None, count=None, search_query=None): + hubs = [] + + params = {"includeMarkers": 1} + if search_query: + q = '/hubs/search' + params['query'] = search_query.lower() + if section: + params['sectionId'] = section + + if count is not None: + params['limit'] = count + else: + q = '/hubs' + if section: + if section == 'playlists': + audio = plexlibrary.AudioPlaylistHub(False, server=self.server) + video = plexlibrary.VideoPlaylistHub(False, server=self.server) + if audio.items: + hubs.append(audio) + if video.items: + hubs.append(video) + return hubs + else: + q = '/hubs/sections/%s' % section + + if count is not None: + params['count'] = count + + data = self.query(q, params=params) + container = plexobjects.PlexContainer(data, initpath=q, server=self, address=q) + + for elem in data: + hubs.append(plexlibrary.Hub(elem, server=self, container=container)) + return hubs + + def playlists(self, start=0, size=10, hub=None): + try: + return plexobjects.listItems(self, '/playlists/all') + except exceptions.BadRequest: + return None + + @property + def library(self): + if self.platform == 'cloudsync': + return plexlibrary.Library(None, server=self) + else: + return plexlibrary.Library(self.query('/library/'), server=self) + + @property + def sessions(self): + if self.owned: + return plexobjects.listItems(self, '/status/sessions') + raise exceptions.ServerNotOwned + + def findVideoSession(self, client_id, rating_key): + for item in self.sessions: + if item.session and item.session.id == client_id and item.ratingKey == rating_key: + return item + + def buildUrl(self, path, includeToken=False): + if self.activeConnection: + return self.activeConnection.buildUrl(self, path, includeToken) + else: + util.WARN_LOG("Server connection is None, returning an empty url") + return "" + + def query(self, path, method=None, **kwargs): + method = method or self.session.get + + limit = kwargs.pop("limit", None) + params = kwargs.pop("params", None) + if params: + if limit is None: + limit = params.get("limit", None) + path += util.joinArgs(params, '?' not in path) + + offset = kwargs.pop("offset", None) + if kwargs: + path += util.joinArgs(kwargs, '?' not in path) + kwargs.clear() + + url = self.buildUrl(path, includeToken=True) + + # If URL is empty, try refresh resources and return empty set for now + if not url: + util.WARN_LOG("Empty server url, returning None and refreshing resources") + util.MANAGER.refreshResources(True) + return None + + # add offset/limit + offset = offset or 0 + + if limit is not None: + url = http.addUrlParam(url, "X-Plex-Container-Start=%s" % offset) + url = http.addUrlParam(url, "X-Plex-Container-Size=%s" % limit) + + util.LOG('{0} {1}'.format(method.__name__.upper(), re.sub('X-Plex-Token=[^&]+', 'X-Plex-Token=****', url))) + try: + response = method(url, **kwargs) + if response.status_code not in (200, 201): + codename = http.status_codes.get(response.status_code, ['Unknown'])[0] + raise exceptions.BadRequest('({0}) {1}'.format(response.status_code, codename)) + data = response.text.encode('utf8') + except asyncadapter.TimeoutException: + util.ERROR() + util.MANAGER.refreshResources(True) + return None + except (http.requests.ConnectionError, urllib3.exceptions.ProtocolError): + util.ERROR() + return None + except asyncadapter.CanceledException: + return None + + return ElementTree.fromstring(data) if data else None + + def getImageTranscodeURL(self, path, width, height, **extraOpts): + if not path: + return '' + + eOpts = {"minSize": 1, "upscale": 1} + eOpts.update(extraOpts) + + params = ("&width=%s&height=%s" % (width, height)) + ''.join(["&%s=%s" % (key, eOpts[key]) for key in eOpts]) + + if "://" in path: + imageUrl = self.convertUrlToLoopBack(path) + else: + imageUrl = "http://127.0.0.1:" + self.getLocalServerPort() + path + + path = "/photo/:/transcode?url=" + compat.quote_plus(imageUrl) + params + + # Try to use a better server to transcode for synced servers + if self.synced: + from . import plexservermanager + selectedServer = plexservermanager.MANAGER.getTranscodeServer("photo") + if selectedServer: + return selectedServer.buildUrl(path, True) + + if self.activeConnection: + return self.activeConnection.simpleBuildUrl(self, path) + else: + util.WARN_LOG("Server connection is None, returning an empty url") + return "" + + def isReachable(self, onlySupported=True): + if onlySupported and not self.isSupported: + return False + + return self.activeConnection and self.activeConnection.state == plexresource.ResourceConnection.STATE_REACHABLE + + def isLocalConnection(self): + return self.activeConnection and (self.sameNetwork or self.activeConnection.isLocal) + + def isRequestToServer(self, url): + if not self.activeConnection: + return False + + if ':' in self.activeConnection.address[8:]: + schemeAndHost = self.activeConnection.address.rsplit(':', 1)[0] + else: + schemeAndHost = self.activeConnection.address + + return url.startswith(schemeAndHost) + + def getToken(self): + # It's dangerous to use for each here, because it may reset the index + # on self.connections when something else was in the middle of an iteration. + + for i in range(len(self.connections)): + try: + conn = self.connections[i] + except IndexError: + continue + if conn.token: + return conn.token + + return None + + def getLocalServerPort(self): + # TODO(schuyler): The correct thing to do here is to iterate over local + # connections and pull out the port. For now, we're always returning 32400. + + return '32400' + + def collectDataFromRoot(self, data): + # Make sure we're processing data for our server, and not some other + # server that happened to be at the same IP. + if self.uuid != data.attrib.get('machineIdentifier'): + util.LOG("Got a reachability response, but from a different server") + return False + + self.serverClass = data.attrib.get('serverClass') + self.supportsAudioTranscoding = data.attrib.get('transcoderAudio') == '1' + self.supportsVideoTranscoding = data.attrib.get('transcoderVideo') == '1' or data.attrib.get('transcoderVideoQualities') + self.supportsVideoRemuxOnly = data.attrib.get('transcoderVideoRemuxOnly') == '1' + self.supportsPhotoTranscoding = data.attrib.get('transcoderPhoto') == '1' or ( + not data.attrib.get('transcoderPhoto') and not self.synced and not self.isSecondary() + ) + self.allowChannelAccess = data.attrib.get('allowChannelAccess') == '1' or ( + not data.attrib.get('allowChannelAccess') and self.owned and not self.synced and not self.isSecondary() + ) + self.supportsScrobble = not self.isSecondary() or self.synced + self.allowsMediaDeletion = not self.synced and self.owned and data.attrib.get('allowMediaDeletion') == '1' + self.multiuser = data.attrib.get('multiuser') == '1' + self.name = data.attrib.get('friendlyName') or self.name + self.platform = data.attrib.get('platform') + + # TODO(schuyler): Process transcoder qualities + + self.rawVersion = data.attrib.get('version') + if self.rawVersion: + self.versionNorm = util.normalizedVersion(self.rawVersion) + + features = { + 'mkvTranscode': '0.9.11.11', + 'themeTranscode': '0.9.14.0', + 'allPartsStreamSelection': '0.9.12.5', + 'claimServer': '0.9.14.2', + 'streamingBrain': '1.2.0' + } + + for f, v in features.items(): + if util.normalizedVersion(v) <= self.versionNorm: + self.features[f] = True + + appMinVer = util.INTERFACE.getGlobal('minServerVersionArr', '0.0.0.0') + self.isSupported = self.isSecondary() or util.normalizedVersion(appMinVer) <= self.versionNorm + + util.DEBUG_LOG("Server information updated from reachability check: {0}".format(self)) + + return True + + def updateReachability(self, force=True, allowFallback=False): + if not force and self.activeConnection and self.activeConnection.state != plexresource.ResourceConnection.STATE_UNKNOWN: + return + + util.LOG('Updating reachability for {0}: conns={1}, allowFallback={2}'.format(repr(self.name), len(self.connections), allowFallback)) + + epoch = time.time() + retrySeconds = 60 + minSeconds = 10 + for i in range(len(self.connections)): + conn = self.connections[i] + diff = epoch - (conn.lastTestedAt or 0) + if conn.hasPendingRequest: + util.DEBUG_LOG("Skip reachability test for {0} (has pending request)".format(conn)) + elif (diff < minSeconds or (not self.isSecondary() and self.isReachable() and diff < retrySeconds)) and \ + not conn.state == "unauthorized": + util.DEBUG_LOG("Skip reachability test for {0} (checked {1} secs ago)".format(conn, diff)) + elif conn.testReachability(self, allowFallback): + self.pendingReachabilityRequests += 1 + if conn.isSecure: + self.pendingSecureRequests += 1 + + if self.pendingReachabilityRequests == 1: + self.trigger("started:reachability") + + if self.pendingReachabilityRequests <= 0: + self.trigger("completed:reachability") + + def cancelReachability(self): + for i in range(len(self.connections)): + conn = self.connections[i] + conn.cancelReachability() + + def onReachabilityResult(self, connection): + connection.lastTestedAt = time.time() + connection.hasPendingRequest = None + self.pendingReachabilityRequests -= 1 + if connection.isSecure: + self.pendingSecureRequests -= 1 + + util.DEBUG_LOG("Reachability result for {0}: {1} is {2}".format(repr(self.name), connection.address, connection.state)) + + # Noneate active connection if the state is unreachable + if self.activeConnection and self.activeConnection.state != plexresource.ResourceConnection.STATE_REACHABLE: + self.activeConnection = None + + # Pick a best connection. If we already had an active connection and + # it's still reachable, stick with it. (replace with local if + # available) + best = self.activeConnection + for i in range(len(self.connections) - 1, -1, -1): + try: + conn = self.connections[i] + except IndexError: + continue + + util.DEBUG_LOG("Connection score: {0}, {1}".format(conn.address, conn.getScore(True))) + + if not best or conn.getScore() > best.getScore(): + best = conn + + if best and best.state == best.STATE_REACHABLE: + if (best.isSecure or util.LOCAL_OVER_SECURE) or self.pendingSecureRequests <= 0: + util.DEBUG_LOG("Using connection for {0} for now: {1}".format(repr(self.name), best.address)) + self.activeConnection = best + else: + util.DEBUG_LOG("Found a good connection for {0}, but holding out for better".format(repr(self.name))) + + if self.pendingReachabilityRequests <= 0: + # Retest the server with fallback enabled. hasFallback will only + # be True if there are available insecure connections and fallback + # is allowed. + + if self.hasFallback: + self.updateReachability(False, True) + else: + self.trigger("completed:reachability") + + util.LOG("Active connection for {0} is {1}".format(repr(self.name), self.activeConnection)) + + from . import plexservermanager + plexservermanager.MANAGER.updateReachabilityResult(self, bool(self.activeConnection)) + + def markAsRefreshing(self): + for i in range(len(self.connections)): + conn = self.connections[i] + conn.refreshed = False + + def markUpdateFinished(self, source): + # Any connections for the given source which haven't been refreshed should + # be removed. Since removing from a list is hard, we'll make a new list. + toKeep = [] + hasSecureConn = False + + for i in range(len(self.connections)): + try: + conn = self.connections[i] + except IndexError: + util.DEBUG_LOG("Connection lost during iteration") + continue + if not conn.refreshed: + conn.sources = conn.sources & (~source) + + # If we lost our plex.tv connection, don't remember the token. + if source == conn.SOURCE_MYPLEX: + conn.token = None + + if conn.sources: + if conn.address[:5] == "https": + hasSecureConn = True + toKeep.append(conn) + else: + util.DEBUG_LOG("Removed connection {0} for {1} after updating connections for {2}".format(conn, repr(self.name), source)) + if conn == self.activeConnection: + util.DEBUG_LOG("Active connection lost") + self.activeConnection = None + + # Update fallback flag if our connections have changed + if len(toKeep) != len(self.connections): + for conn in toKeep: + conn.isFallback = hasSecureConn and conn.address[:5] != "https" and not util.LOCAL_OVER_SECURE + + self.connections = toKeep + + return len(self.connections) > 0 + + def merge(self, other): + # Wherever this other server came from, assume its information is better + # except for manual connections. + + if other.sourceType != plexresource.ResourceConnection.SOURCE_MANUAL: + self.name = other.name + self.versionNorm = other.versionNorm + self.sameNetwork = other.sameNetwork + + if other.sourceType == plexresource.ResourceConnection.SOURCE_MANUAL and util.LOCAL_OVER_SECURE: + self.sameNetwork = other.sameNetwork + + # Merge connections + for otherConn in other.connections: + merged = False + for i in range(len(self.connections)): + myConn = self.connections[i] + if myConn == otherConn: + myConn.merge(otherConn) + merged = True + break + + if not merged: + self.connections.append(otherConn) + + # If the other server has a token, then it came from plex.tv, which + # means that its ownership information is better than ours. But if + # it was discovered, then it may incorrectly claim to be owned, so + # we stick with whatever we already had. + + if other.getToken(): + self.owned = other.owned + self.owner = other.owner + + def supportsFeature(self, feature): + return feature in self.features + + def getVersion(self): + if not self.versionNorm: + return '' + + return str(self.versionNorm) + + def convertUrlToLoopBack(self, url): + # If the URL starts with our server URL, replace it with 127.0.0.1:32400. + if self.isRequestToServer(url): + url = 'http://127.0.0.1:32400/' + url.split('://', 1)[-1].split('/', 1)[-1] + return url + + def resetLastTest(self): + for i in range(len(self.connections)): + conn = self.connections[i] + conn.lastTestedAt = None + + def isSecondary(self): + return self.serverClass == "secondary" + + def getLibrarySectionByUuid(self, uuid=None): + if not uuid: + return None + return self.librariesByUuid[uuid] + + def setLibrarySectionByUuid(self, uuid, library): + self.librariesByUuid[uuid] = library + + def hasInsecureConnections(self): + if util.INTERFACE.getPreference('allow_insecure') == 'always': + return False + + # True if we have any insecure connections we have disallowed + for i in range(len(self.connections)): + conn = self.connections[i] + if not conn.isSecure and conn.state == conn.STATE_INSECURE: + return True + + return False + + def hasSecureConnections(self): + for i in range(len(self.connections)): + conn = self.connections[i] + if conn.isSecure: + return True + + return False + + def getLibrarySectionPrefs(self, uuid): + # TODO: Make sure I did this right - ruuk + librarySection = self.getLibrarySectionByUuid(uuid) + + if librarySection and librarySection.key: + # Query and store the prefs only when asked for. We could just return the + # items, but it'll be more useful to store the pref ids in an associative + # array for ease of selecting the pref we need. + + if not librarySection.sectionPrefs: + path = "/library/sections/{0}/prefs".format(librarySection.key) + data = self.query(path) + if data: + librarySection.sectionPrefs = {} + for elem in data: + item = plexobjects.buildItem(self, elem, path) + if item.id: + librarySection.sectionPrefs[item.id] = item + + return librarySection.sectionPrefs + + return None + + def swizzleUrl(self, url, includeToken=False): + m = re.search(r"^\w+://.+?(/.+)", url) + newUrl = m and m.group(1) or None + return self.buildUrl(newUrl or url, includeToken) + + def hasHubs(self): + return self.platform != 'cloudsync' + + @property + def address(self): + return self.activeConnection.address + + @classmethod + def deSerialize(cls, jstring): + try: + serverObj = json.loads(jstring) + except: + util.ERROR() + util.ERROR_LOG("Failed to deserialize PlexServer JSON") + return + + from . import plexconnection + + server = createPlexServerForName(serverObj['uuid'], serverObj['name']) + server.owned = bool(serverObj.get('owned')) + server.sameNetwork = serverObj.get('sameNetwork') + + hasSecureConn = False + for i in range(len(serverObj.get('connections', []))): + conn = serverObj['connections'][i] + if conn['address'][:5] == "https": + hasSecureConn = True + break + + for i in range(len(serverObj.get('connections', []))): + conn = serverObj['connections'][i] + isFallback = hasSecureConn and conn['address'][:5] != "https" and not util.LOCAL_OVER_SECURE + sources = plexconnection.PlexConnection.SOURCE_BY_VAL[conn['sources']] + connection = plexconnection.PlexConnection(sources, conn['address'], conn['isLocal'], conn['token'], isFallback) + + # Keep the secure connection on top + if connection.isSecure and not util.LOCAL_OVER_SECURE: + server.connections.insert(0, connection) + elif not connection.isSecure and util.LOCAL_OVER_SECURE: + server.connections.insert(0, connection) + else: + server.connections.append(connection) + + if conn.get('active'): + server.activeConnection = connection + + return server + + def serialize(self, full=False): + serverObj = { + 'name': self.name, + 'uuid': self.uuid, + 'owned': self.owned, + 'connections': [] + } + + if full: + for conn in self.connections: + serverObj['connections'].append({ + 'sources': conn.sources, + 'address': conn.address, + 'isLocal': conn.isLocal, + 'isSecure': conn.isSecure, + 'token': conn.token + }) + if conn == self.activeConnection: + serverObj['connections'][-1]['active'] = True + else: + serverObj['connections'] = [{ + 'sources': self.activeConnection.sources, + 'address': self.activeConnection.address, + 'isLocal': self.activeConnection.isLocal, + 'isSecure': self.activeConnection.isSecure, + 'token': self.activeConnection.token or self.getToken(), + 'active': True + }] + + return json.dumps(serverObj) + + +def dummyPlexServer(): + return createPlexServer() + + +def createPlexServer(): + return PlexServer() + + +def createPlexServerForConnection(conn): + obj = createPlexServer() + obj.connections.append(conn) + obj.activeConnection = conn + return obj + + +def createPlexServerForName(uuid, name): + obj = createPlexServer() + obj.uuid = uuid + obj.name = name + return obj + + +def createPlexServerForResource(resource): + # resource.__class__ = PlexServer + # resource.server = resource + # resource.session = http.Session() + return resource diff --git a/script.plexmod/lib/_included_packages/plexnet/plexservermanager.py b/script.plexmod/lib/_included_packages/plexnet/plexservermanager.py new file mode 100644 index 000000000..4738a55ce --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexservermanager.py @@ -0,0 +1,651 @@ +from __future__ import absolute_import +import json + +from . import http +from . import plexconnection +from . import plexresource +from . import plexserver +from . import signalsmixin +from . import callback +from . import plexapp +from . import gdm +from . import util +from six.moves import range + + +class SearchContext(dict): + def __getattr__(self, attr): + return self.get(attr) + + def __setattr__(self, attr, value): + self[attr] = value + + +class PlexServerManager(signalsmixin.SignalsMixin): + def __init__(self): + signalsmixin.SignalsMixin.__init__(self) + # obj.Append(ListenersMixin()) + self.serversByUuid = {} + self.selectedServer = None + self.transcodeServer = None + self.channelServer = None + self.deferReachabilityTimer = None + + self.startSelectedServerSearch() + self.loadState() + + plexapp.util.APP.on("change:user", callback.Callable(self.onAccountChange)) + plexapp.util.APP.on("change:allow_insecure", callback.Callable(self.onSecurityChange)) + plexapp.util.APP.on("change:manual_connections", callback.Callable(self.onManualConnectionChange)) + + def getSelectedServer(self): + return self.selectedServer + + def setSelectedServer(self, server, force=False): + # Don't do anything if the server is already selected. + if self.selectedServer and self.selectedServer == server: + return False + + if server: + # Don't select servers that don't have connections. + if not server.activeConnection: + return False + + # Don't select servers that are not supported + if not server.isSupported: + return False + + if not self.selectedServer or force: + util.LOG("Setting selected server to {0}".format(server)) + self.selectedServer = server + + # Update our saved state. + self.saveState(setPreferred=True) + + # Notify anyone who might care. + util.APP.trigger("change:selectedServer", server=server) + + return True + + return False + + def getServer(self, uuid=None): + if uuid is None: + return None + elif uuid == "myplex": + from . import myplexserver + return myplexserver.MyPlexServer() + else: + return self.serversByUuid[uuid] + + def getServers(self): + servers = [] + for uuid in list(self.serversByUuid.keys()): + if uuid != "myplex": + servers.append(self.serversByUuid[uuid]) + + return servers + + def hasPendingRequests(self): + for server in self.getServers(): + if server.pendingReachabilityRequests: + return True + + return False + + def removeServer(self, server): + del self.serversByUuid[server.uuid] + + self.trigger('remove:server') + + if server == self.selectedServer: + util.LOG("The selected server went away") + self.setSelectedServer(None, force=True) + + if server == self.transcodeServer: + util.LOG("The selected transcode server went away") + self.transcodeServer = None + + if server == self.channelServer: + util.LOG("The selected channel server went away") + self.channelServer = None + + def updateFromConnectionType(self, servers, source): + self.markDevicesAsRefreshing() + + for server in servers: + self.mergeServer(server) + + if self.searchContext and source in self.searchContext.waitingForResources: + #self.searchContext.waitingForResources = False + self.searchContext.waitingForResources.remove(source) + + if not self.searchContext.waitingForResources: + self.deviceRefreshComplete(source) + self.updateReachability(True, True) + self.saveState() + + def updateFromDiscovery(self, server): + merged = self.mergeServer(server) + + if not merged.activeConnection: + merged.updateReachability(False, True) + else: + # self.notifyAboutDevice(merged, True) + pass + + def markDevicesAsRefreshing(self): + for uuid in list(self.serversByUuid.keys()): + self.serversByUuid[uuid].markAsRefreshing() + + def mergeServer(self, server): + if server.uuid in self.serversByUuid: + existing = self.serversByUuid[server.uuid] + existing.merge(server) + util.DEBUG_LOG("Merged {0}".format(repr(server.name))) + return existing + else: + self.serversByUuid[server.uuid] = server + util.DEBUG_LOG("Added new server {0}".format(repr(server.name))) + self.trigger("new:server", server=server) + return server + + def deviceRefreshComplete(self, source): + toRemove = [] + for uuid in list(self.serversByUuid.keys()): + if not self.serversByUuid[uuid].markUpdateFinished(source): + toRemove.append(uuid) + + for uuid in toRemove: + if uuid not in self.serversByUuid: + util.DEBUG_LOG("Server {} lost - removing".format(uuid)) + continue + + server = self.serversByUuid[uuid] + + util.DEBUG_LOG("Server {0} has no more connections - removing".format(repr(server.name))) + # self.notifyAboutDevice(server, False) + self.removeServer(server) + + def updateReachability(self, force=False, preferSearch=False, defer=False): + # We don't need to test any servers unless we are signed in and authenticated. + if not plexapp.ACCOUNT.isAuthenticated and plexapp.ACCOUNT.isActive(): + util.LOG("Ignore testing server reachability until we're authenticated") + return + + # To improve reachability performance and app startup, we'll try to test the + # preferred server first, and defer the connection tests for a few seconds. + + hasPreferredServer = bool(self.searchContext.preferredServer) + preferredServerExists = hasPreferredServer and self.searchContext.preferredServer in self.serversByUuid + + if preferSearch and hasPreferredServer and preferredServerExists: + # Update the preferred server immediately if requested and exits + util.LOG("Updating reachability for preferred server: force={0}".format(force)) + self.serversByUuid[self.searchContext.preferredServer].updateReachability(force) + self.deferUpdateReachability() + elif defer: + self.deferUpdateReachability() + elif hasPreferredServer and not preferredServerExists and gdm.DISCOVERY.isActive(): + # Defer the update if requested or if GDM discovery is enabled and + # active while the preferred server doesn't exist. + + util.LOG("Defer update reachability until GDM has finished to help locate the preferred server") + self.deferUpdateReachability(True, False) + else: + if self.deferReachabilityTimer: + self.deferReachabilityTimer.cancel() + self.deferReachabilityTimer = None + + util.LOG("Updating reachability for all devices: force={0}".format(force)) + for uuid in list(self.serversByUuid.keys()): + self.serversByUuid[uuid].updateReachability(force) + + def cancelReachability(self): + if self.deferReachabilityTimer: + self.deferReachabilityTimer.cancel() + self.deferReachabilityTimer = None + + for uuid in list(self.serversByUuid.keys()): + self.serversByUuid[uuid].cancelReachability() + + def updateReachabilityResult(self, server, reachable=False): + searching = not self.selectedServer and self.searchContext + + if reachable: + # If we're in the middle of a search for our selected server, see if + # this is a candidate. + self.trigger('reachable:server', server=server) + if searching: + # If this is what we were hoping for, select it + if server.uuid == self.searchContext.preferredServer: + self.setSelectedServer(server, True) + elif server.synced: + self.searchContext.fallbackServer = server + elif self.compareServers(self.searchContext.bestServer, server) < 0: + self.searchContext.bestServer = server + else: + # If this is what we were hoping for, see if there are any more pending + # requests to hope for. + + if searching and server.uuid == self.searchContext.preferredServer and server.pendingReachabilityRequests <= 0: + self.searchContext.preferredServer = None + + if server == self.selectedServer: + util.LOG("Selected server is not reachable") + self.setSelectedServer(None, True) + + if server == self.transcodeServer: + util.LOG("The selected transcode server is not reachable") + self.transcodeServer = None + + if server == self.channelServer: + util.LOG("The selected channel server is not reachable") + self.channelServer = None + + # See if we should settle for the best we've found so far. + self.checkSelectedServerSearch() + + def checkSelectedServerSearch(self, skip_preferred=False, skip_owned=False): + if self.selectedServer: + return self.selectedServer + elif self.searchContext: + # If we're still waiting on the resources response then there's no + # reason to settle, so don't even iterate over our servers. + + if self.searchContext.waitingForResources: + util.DEBUG_LOG("Still waiting for plex.tv resources") + return + + waitingForPreferred = False + waitingForOwned = False + waitingForAnything = False + waitingToTestAll = bool(self.deferReachabilityTimer) + + if skip_preferred: + self.searchContext.preferredServer = None + if self.deferReachabilityTimer: + self.deferReachabilityTimer.cancel() + self.deferReachabilityTimer = None + + if not skip_owned: + # Iterate over all our servers and see if we're waiting on any results + servers = self.getServers() + pendingCount = 0 + for server in servers: + if server.pendingReachabilityRequests > 0: + pendingCount += server.pendingReachabilityRequests + if server.uuid == self.searchContext.preferredServer: + waitingForPreferred = True + elif server.owned: + waitingForOwned = True + else: + waitingForAnything = True + + pendingString = "{0} pending reachability tests".format(pendingCount) + + if waitingForPreferred: + util.LOG("Still waiting for preferred server: " + pendingString) + elif waitingToTestAll: + util.LOG("Preferred server not reachable, testing all servers now") + self.updateReachability(True, False, False) + elif waitingForOwned and (not self.searchContext.bestServer or not self.searchContext.bestServer.owned): + util.LOG("Still waiting for an owned server: " + pendingString) + elif waitingForAnything and not self.searchContext.bestServer: + util.LOG("Still waiting for any server: {0}".format(pendingString)) + else: + # No hope for anything better, let's select what we found + util.LOG("Settling for the best server we found") + self.setSelectedServer(self.searchContext.bestServer or self.searchContext.fallbackServer, True) + return self.selectedServer + + def compareServers(self, first, second): + if not first or not first.isSupported: + return second and -1 or 0 + elif not second: + return 1 + elif first.owned != second.owned: + return first.owned and 1 or -1 + elif first.isLocalConnection() != second.isLocalConnection(): + return first.isLocalConnection() and 1 or -1 + else: + return 0 + + def loadState(self): + jstring = util.INTERFACE.getRegistry("PlexServerManager") + if not jstring: + return + + try: + obj = json.loads(jstring) + except: + util.ERROR() + obj = None + + if not obj: + util.ERROR_LOG("Failed to parse PlexServerManager JSON") + return + + for serverObj in obj['servers']: + server = plexserver.createPlexServerForName(serverObj['uuid'], serverObj['name']) + server.owned = bool(serverObj.get('owned')) + server.sameNetwork = serverObj.get('sameNetwork') + + hasSecureConn = False + for i in range(len(serverObj.get('connections', []))): + conn = serverObj['connections'][i] + if conn['address'][:5] == "https": + hasSecureConn = True + break + + for i in range(len(serverObj.get('connections', []))): + conn = serverObj['connections'][i] + isFallback = hasSecureConn and conn['address'][:5] != "https" and not util.LOCAL_OVER_SECURE + sources = plexconnection.PlexConnection.SOURCE_BY_VAL[conn['sources']] + connection = plexconnection.PlexConnection(sources, conn['address'], conn['isLocal'], conn['token'], isFallback) + + # Keep the secure connection on top + if connection.isSecure and not util.LOCAL_OVER_SECURE: + server.connections.insert(0, connection) + elif not connection.isSecure and util.LOCAL_OVER_SECURE: + server.connections.insert(0, connection) + else: + server.connections.append(connection) + + self.serversByUuid[server.uuid] = server + + util.LOG("Loaded {0} servers from registry".format(len(obj['servers']))) + self.updateReachability(False, True) + + def saveState(self, setPreferred=False): + # Serialize our important information to JSON and save it to the registry. + # We'll always update server info upon connecting, so we don't need much + # info here. We do have to use roArray instead of roList, because Brightscript. + + obj = {} + + servers = self.getServers() + obj['servers'] = [] + + for server in servers: + # Don't save secondary servers. They should be discovered through GDM or myPlex. + if not server.isSecondary(): + serverObj = { + 'name': server.name, + 'uuid': server.uuid, + 'owned': server.owned, + 'sameNetwork': server.sameNetwork, + 'connections': [] + } + + for i in range(len(server.connections)): + conn = server.connections[i] + serverObj['connections'].append({ + 'sources': conn.sources, + 'address': conn.address, + 'isLocal': conn.isLocal, + 'isSecure': conn.isSecure, + 'token': conn.token + }) + + obj['servers'].append(serverObj) + + if self.selectedServer and not self.selectedServer.synced and not self.selectedServer.isSecondary() \ + and setPreferred: + util.INTERFACE.setPreference("lastServerId.{}".format(plexapp.ACCOUNT.ID), self.selectedServer.uuid) + + util.INTERFACE.setRegistry("PlexServerManager", json.dumps(obj)) + + def clearState(self): + util.INTERFACE.setRegistry("PlexServerManager", '') + + def isValidForTranscoding(self, server): + return server and server.activeConnection and server.owned and not server.synced and not server.isSecondary() + + def getChannelServer(self): + if not self.channelServer or not self.channelServer.isReachable(): + self.channelServer = None + + # Attempt to find a server that supports channels and transcoding + for s in self.getServers(): + if s.supportsVideoTranscoding and s.allowChannelAccess and s.isReachable() and self.compareServers(self.channelServer, s) < 0: + self.channelServer = s + + # Fallback to any server that supports channels + if not self.channelServer: + for s in self.getServers(): + if s.allowChannelAccess and s.isReachable() and self.compareServers(self.channelServer, s) < 0: + self.channelServer = s + + if self.channelServer: + util.LOG("Setting channel server to {0}".format(self.channelServer)) + + return self.channelServer + + def getTranscodeServer(self, transcodeType=None): + if not self.selectedServer: + return None + + transcodeMap = { + 'audio': "supportsAudioTranscoding", + 'video': "supportsVideoTranscoding", + 'photo': "supportsPhotoTranscoding" + } + transcodeSupport = transcodeMap[transcodeType] + + # Try to use a better transcoding server for synced or secondary servers + if self.selectedServer.synced or self.selectedServer.isSecondary(): + if self.transcodeServer and self.transcodeServer.isReachable(): + return self.transcodeServer + + self.transcodeServer = None + for server in self.getServers(): + if not server.synced and server.isReachable() and self.compareServers(self.transcodeServer, server) < 0: + if not transcodeSupport or server.transcodeSupport: + self.transcodeServer = server + + if self.transcodeServer: + transcodeTypeString = transcodeType or '' + util.LOG("Found a better {0} transcode server than {1}, using: {2}".format(transcodeTypeString, self.selectedserver, self.transcodeServer)) + return self.transcodeServer + + return self.selectedServer + + def startSelectedServerSearch(self, reset=False, ID=None): + if reset: + self.selectedServer = None + self.transcodeServer = None + self.channelServer = None + + ID = ID is not None and ID or plexapp.ACCOUNT.ID + pServ = util.INTERFACE.getPreference("lastServerId.{}".format(ID), '') + util.DEBUG_LOG("Preferred server for {0} is: {1}".format(ID, pServ)) + # Keep track of some information during our search + + waitFor = [] + if plexapp.ACCOUNT.isSignedIn: + waitFor.append(plexresource.ResourceConnection.SOURCE_MYPLEX) + + if util.LOCAL_OVER_SECURE and self.getManualConnections(): + waitFor.append(plexresource.ResourceConnection.SOURCE_MANUAL) + + self.searchContext = SearchContext({ + 'bestServer': None, + 'preferredServer': pServ, + 'waitingForResources': waitFor + }) + + util.LOG("Starting selected server search, hoping for {0}".format(self.searchContext.preferredServer)) + if util.LOCAL_OVER_SECURE: + util.WARN_LOG("Preferring local server connections over secure ones!") + + def onAccountChange(self, account, reallyChanged=False): + # Clear any AudioPlayer data before invalidating the active server + if reallyChanged: + # AudioPlayer().Cleanup() + # PhotoPlayer().Cleanup() + + util.DEBUG_LOG("Account really changed, clearing all servers") + + # Clear selected and transcode servers on user change + self.selectedServer = None + self.transcodeServer = None + self.channelServer = None + self.cancelReachability() + + if account.isSignedIn: + # If the user didn't really change, such as selecting the previous user + # on the lock screen, then we don't need to clear anything. We can + # avoid a costly round of reachability checks. + + if not reallyChanged: + return + + # A request to refresh resources has already been kicked off. We need + # to clear out any connections for the previous user and then start + # our selected server search. + + self.updateFromConnectionType([], plexresource.ResourceConnection.SOURCE_MYPLEX) + self.updateFromConnectionType([], plexresource.ResourceConnection.SOURCE_DISCOVERED) + self.updateFromConnectionType([], plexresource.ResourceConnection.SOURCE_MANUAL) + + self.startSelectedServerSearch(True, ID=account.ID) + + if reallyChanged: + util.DEBUG_LOG("User really changed, refreshing resources now") + plexapp.refreshResources() + else: + # Clear servers/connections from plex.tv + self.updateFromConnectionType([], plexresource.ResourceConnection.SOURCE_MYPLEX) + + def deferUpdateReachability(self, addTimer=True, logInfo=True): + if addTimer and not self.deferReachabilityTimer: + self.deferReachabilityTimer = plexapp.createTimer(1000, callback.Callable(self.onDeferUpdateReachabilityTimer), repeat=True) + util.APP.addTimer(self.deferReachabilityTimer) + else: + if self.deferReachabilityTimer: + self.deferReachabilityTimer.reset() + + if self.deferReachabilityTimer and logInfo: + util.LOG('Defer update reachability for all devices a few seconds: GDMactive={0}'.format(gdm.DISCOVERY.isActive())) + + def onDeferUpdateReachabilityTimer(self): + if not self.selectedServer and self.searchContext: + for server in self.getServers(): + if server.pendingReachabilityRequests > 0 and server.uuid == self.searchContext.preferredServer: + util.DEBUG_LOG( + 'Still waiting on {0} responses from preferred server: {1}'.format( + server.pendingReachabilityRequests, self.searchContext.preferredServer + ) + ) + return + + if self.deferReachabilityTimer: + self.deferReachabilityTimer.cancel() + self.deferReachabilityTimer = None + self.updateReachability(True, False, False) + + def resetLastTest(self): + for uuid in list(self.serversByUuid.keys()): + self.serversByUuid[uuid].resetLastTest() + + def clearServers(self): + self.cancelReachability() + self.serversByUuid = {} + self.saveState() + + def onSecurityChange(self, value=None): + # If the security policy changes, then we will need to allow all + # connections to be retested by resetting the last test. We can + # simply call `self.resetLastTest()` to allow all connection to be + # tested when the server dropdown is enable, but we may as well + # test all the connections immediately. + + plexapp.refreshResources(True) + + def onManualConnectionChange(self, value=None): + # Clear all manual connections on change. We will keep the selected + # server around temporarily if it's a manual connection regardless + # if it's been removed. + + # Remember the current server in case it's removed + server = self.getSelectedServer() + activeConn = [] + if server and server.activeConnection: + activeConn.append(server.activeConnection) + + # Clear all manual connections + self.updateFromConnectionType([], plexresource.ResourceConnection.SOURCE_MANUAL) + + # Reused the previous selected server if our manual connection has gone away + if not self.getSelectedServer() and activeConn.sources == plexresource.ResourceConnection.SOURCE_MANUAL: + server.activeConnection = activeConn + server.connections.append(activeConn) + self.setSelectedServer(server, True) + + def refreshManualConnections(self): + manualConnections = self.getManualConnections() + if not manualConnections: + util.DEBUG_LOG("No manual connections.") + return + + util.LOG("Refreshing {0} manual connections".format(len(manualConnections))) + + for conn in manualConnections: + # Default to http, as the server will need to be signed in for https to work, + # so the client should too. We'd also have to allow hostname entry, instead of + # IP address for the cert to validate. + + proto = "http" + port = conn.port or "32400" + serverAddress = "{0}://{1}:{2}".format(proto, conn.connection, port) + + request = http.HttpRequest(serverAddress + "/identity") + context = request.createRequestContext("manual_connections", callback.Callable(self.onManualConnectionsResponse)) + context.serverAddress = serverAddress + context.address = conn.connection + context.proto = proto + context.port = port + util.APP.startRequest(request, context) + + def onManualConnectionsResponse(self, request, response, context): + if not response.isSuccess(): + return + + data = response.getBodyXml() + if data is not None: + serverAddress = context.serverAddress + util.DEBUG_LOG("Received manual connection response for {0}".format(serverAddress)) + + machineID = data.attrib.get('machineIdentifier') + name = context.address + if not name or not machineID: + return + + # TODO(rob): Do we NOT want to consider manual connections local? + conn = plexconnection.PlexConnection(plexresource.ResourceConnection.SOURCE_MANUAL, serverAddress, True, None) + server = plexserver.createPlexServerForConnection(conn) + server.uuid = machineID + server.name = name + server.sourceType = plexresource.ResourceConnection.SOURCE_MANUAL + self.updateFromConnectionType([server], plexresource.ResourceConnection.SOURCE_MANUAL) + + def getManualConnections(self): + manualConnections = [] + + jstring = util.INTERFACE.getPreference('manual_connections') + if jstring: + connections = json.loads(jstring) + if isinstance(connections, list): + for conn in connections: + conn = util.AttributeDict(conn) + if conn.connection: + manualConnections.append(conn) + + return manualConnections + +# TODO(schuyler): Notifications +# TODO(schuyler): Transcode (and primary) server selection + + +MANAGER = PlexServerManager() diff --git a/script.plexmod/lib/_included_packages/plexnet/plexstream.py b/script.plexmod/lib/_included_packages/plexnet/plexstream.py new file mode 100644 index 000000000..33b5352f6 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/plexstream.py @@ -0,0 +1,195 @@ +from __future__ import absolute_import +from . import plexobjects +from . import util + + +class PlexStream(plexobjects.PlexObject): + # Constants + TYPE_UNKNOWN = 0 + TYPE_VIDEO = 1 + TYPE_AUDIO = 2 + TYPE_SUBTITLE = 3 + TYPE_LYRICS = 4 + + streamTypeNames = ( + "Unknown", "VideoStream", "AudioStream", "SubtitleStream", "LyricsStream" + ) + + # We have limited font support, so make a very modest effort at using + # English names for common unsupported languages. + + SAFE_LANGUAGE_NAMES = { + 'ara': "Arabic", + 'arm': "Armenian", + 'bel': "Belarusian", + 'ben': "Bengali", + 'bul': "Bulgarian", + 'chi': "Chinese", + 'cze': "Czech", + 'gre': "Greek", + 'heb': "Hebrew", + 'hin': "Hindi", + 'jpn': "Japanese", + 'kor': "Korean", + 'rus': "Russian", + 'srp': "Serbian", + 'tha': "Thai", + 'ukr': "Ukrainian", + 'yid': "Yiddish" + } + + def reload(self): + pass + + def getTitle(self, translate_func=util.dummyTranslate): + streamType = self.streamType.asInt() + + if streamType == self.TYPE_SUBTITLE \ + and util.INTERFACE.getPreference('subtitle_use_extended_title', True) \ + and self.extendedDisplayTitle: + return self.extendedDisplayTitle + + title = self.getLanguageName(translate_func) + + if streamType == self.TYPE_VIDEO: + title = self.getCodec() or translate_func("Unknown") + elif streamType == self.TYPE_AUDIO: + codec = self.getCodec() + channels = self.getChannels(translate_func) + + if codec != "" and channels != "": + title += u" ({0} {1})".format(codec, channels) + elif codec != "" or channels != "": + title += u" ({0}{1})".format(codec, channels) + elif streamType == self.TYPE_SUBTITLE: + extras = [] + + codec = self.getCodec() + if codec: + extras.append(codec) + + if self.sdh: + title += " {}".format(translate_func("SDH")) + + if not self.key: + extras.append(translate_func("Embedded")) + + if self.forced.asBool(): + extras.append(translate_func("Forced")) + + if len(extras) > 0: + title += u" ({0})".format('/'.join(extras)) + elif streamType == self.TYPE_LYRICS: + title = translate_func("Lyrics") + if self.format: + title += u" ({0})".format(self.format) + + return title + + def getCodec(self): + codec = (self.codec or '').lower() + + if codec in ('dca', 'dca-ma', 'dts-hd', 'dts-es', 'dts-hra'): + codec = "DTS" + else: + codec = codec.upper() + + return codec + + def getChannels(self, translate_func=util.dummyTranslate): + channels = self.channels.asInt() + + if channels == 1: + return translate_func("Mono") + elif channels == 2: + return translate_func("Stereo") + elif channels > 0: + return "{0}.1".format(channels - 1) + else: + return "" + + def getLanguageName(self, translate_func=util.dummyTranslate): + code = self.languageCode + + if not code: + return translate_func("Unknown") + + return self.SAFE_LANGUAGE_NAMES.get(code) or self.language or "Unknown" + + def getSubtitlePath(self): + query = "?encoding=utf-8" + + if self.codec == "smi": + query += "&format=srt" + + return self.key + query + + def getSubtitleServerPath(self): + if not self.key: + return None + + return self.getServer().buildUrl(self.getSubtitlePath(), True) + + @property + def embedded(self): + return not bool(self.getSubtitleServerPath()) + + def isSelected(self): + return self.selected.asBool() + + def setSelected(self, selected): + self.selected = plexobjects.PlexValue(selected and '1' or '0') + + @property + def sdh(self): + return self.hearingImpaired or "SDH" in self.title or "SDH" in self.displayTitle \ + or "SDH" in self.extendedDisplayTitle + + @property + def videoCodecRendering(self): + render = "sdr" + + if self.colorTrc == "smpte2084": + if self.DOVIProfile == "8" and self.DOVIBLCompatID == "1": + render = "dv/hdr10" + elif self.DOVIProfile: + render = "dv" + else: + render = "hdr" + elif self.colorTrc == "arib-std-b67": + render = "hlg" + + return render.upper() + + def __str__(self): + return self.getTitle() + + def __repr__(self): + return '<{}: {}>'.format(self.streamTypeNames[self.streamType.asInt()], str(self)) + + def __eq__(self, other): + if not other: + return False + + if self.__class__ != other.__class__: + return False + + for attr in ("streamType", "language", "codec", "channels", "index", "key"): + if getattr(self, attr) != getattr(other, attr): + return False + return True + + +# Synthetic subtitle stream for 'none' + +class NoneStream(PlexStream): + def __init__(self, *args, **kwargs): + PlexStream.__init__(self, None, *args, **kwargs) + self.id = plexobjects.PlexValue("0") + self.streamType = plexobjects.PlexValue(str(self.TYPE_SUBTITLE)) + + def getTitle(self, translate_func=util.dummyTranslate): + return translate_func("None") + + +NONE_STREAM = NoneStream() diff --git a/script.plexmod/lib/_included_packages/plexnet/serverdecision.py b/script.plexmod/lib/_included_packages/plexnet/serverdecision.py new file mode 100644 index 000000000..e1a5344c6 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/serverdecision.py @@ -0,0 +1,102 @@ +from __future__ import absolute_import +from . import mediachoice +from . import util + + +class DecisionFailure(Exception): + def __init__(self, code, reason): + self.code = code + self.reason = reason + + +class ServerDecision(object): + DECISION_DIRECT_PLAY = "directplay" + DECISION_COPY = "copy" + DECISION_TRANSCODE = "transcode" + DIRECT_PLAY_OK = 1000 + TRANSCODE_OK = 1001 + + def __init__(self, original, response, player): + self.original = original + self.response = response + self.player = player + self.item = None + + self.init() + + def init(self): + self.isSupported = self.response.server.supportsFeature("streamingBrain") + for item in self.response.items: + if item and item.media: + self.item = item + self.original.transcodeDecision = mediachoice.MediaChoice(self.item.media[0]) + + # Decision codes and text + self.decisionsCodes = {} + self.decisionsTexts = {} + for key in ["directPlayDecision", "generalDecision", "mdeDecision", "transcodeDecision", "termination"]: + self.decisionsCodes[key] = self.response.container.get(key + "Code", "-1").asInt() + self.decisionsTexts[key] = self.response.container.get(key + "Text") + + util.DEBUG_LOG("Decision codes: {0}".format(self.decisionsCodes)) + + def __str__(self): + if self.isSupported: + obj = [] + for v in self.decisionsTexts.values(): + if v: + obj.append(v) + return ' '.join(obj) + else: + return "Server version does not support decisions." + + def __repr__(self): + return self.__str__() + + def getDecision(self, requireDecision=True): + if not self.item: + # Return no decision. The player will either continue with the original + # or terminate if a valid decision was required. + + if requireDecision: + # Terminate the player by default if there was no decision returned. + code = self.decisionsCodes["generalDecision"] + reason = ' '.join([self.decisionsTexts["transcodeDecision"], self.decisionsTexts["generalDecision"]]) + raise DecisionFailure(code, reason) + + return None + + # Rebuild the original item with the new item. + util.WARN_LOG("Server requested new playback decision: {0}".format(self)) + self.original.rebuild(self.item, self) + return self.original + + def isSuccess(self): + code = self.decisionsCodes["mdeDecision"] + return not self.isSupported or 1000 <= code < 2000 + + def isDecision(self, requireItem=False): + # Server has provided a valid decision if there was a valid decision code + # or if the response returned zero items (could not play). + return self.isSupported and (self.decisionsCodes["mdeDecision"] > -1 or requireItem and not self.item) + + def isTimelineDecision(self): + return self.isSupported and self.item + + def isTermination(self): + return self.isSupported and self.decisionsCodes["termination"] > -1 + + def directPlayOK(self): + return self.decisionsCodes["mdeDecision"] == 1000 + + def getTermination(self): + return { + 'code': str(self.decisionsCodes["termination"]), + 'text': self.decisionsTexts["termination"] or "Unknown" # TODO: Translate Unknown + } + + def getDecisionText(self): + for key in ["mdeDecision", "directPlayDecision", "generalDecision", "transcodeDecision"]: + if self.decisionsTexts.get(key): + return self.decisionsTexts[key] + return None diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/__init__.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/__init__.py new file mode 100644 index 000000000..b9c234ff3 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/__init__.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import +try: + from .signal import Signal + from .slot import Slot + from .exceptions import * +except ImportError: # pragma: no cover + # Possible we are running from setup.py, in which case we're after + # the __version__ string only. + pass + +__version__ = '0.1.1' diff --git a/script.module.protobuf/resources/lib/google/__init__.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/__init__.py similarity index 100% rename from script.module.protobuf/resources/lib/google/__init__.py rename to script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/__init__.py diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/__init__.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/__init__.py new file mode 100644 index 000000000..280e6a2ad --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/__init__.py @@ -0,0 +1 @@ +from .task import Task diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/task.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/task.py new file mode 100644 index 000000000..b6c81c1d2 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/task.py @@ -0,0 +1,76 @@ +from __future__ import absolute_import +import sys +import eventlet +import contexter +import six + + +class Task(object): + @classmethod + def get_or_create(cls, signal, kwargs=None, logger=None): + if not hasattr(cls, '_registry'): + cls._registry = [] + + task = cls(signal, kwargs, logger=logger) + + if task not in cls._registry: + cls._registry.append(task) + + return cls._registry[cls._registry.index(task)] + + def __init__(self, signal, kwargs=None, logger=None): + self.signal = signal + self.kwargs = kwargs or {} + self.logger = logger + self.failures = 0 + self.task_semaphore = eventlet.semaphore.BoundedSemaphore(1) + + def __call__(self, semaphores=None): + semaphores = semaphores or [] + + with contexter.Contexter(self.task_semaphore, *semaphores): + result = self._do() + + if result: + self.failures = 0 + else: + self.failures += 1 + + return result + + def _do(self): + try: + self._emit() + except Exception: + self._exception(*sys.exc_info()) + return False + else: + self._completed() + return True + finally: + self._clean() + + def _clean(self): + pass + + def _completed(self): + if self.logger: + self.logger.info('[%s] Completed' % self) + + def _exception(self, e_type, e_value, e_traceback): + if self.logger: + self.logger.exception('[%s] Raised exception: %s' % ( + self, e_value)) + else: + six.reraise(e_type, e_value, e_traceback) + + def _emit(self): + if self.logger: + self.logger.info('[%s] Running' % self) + self.signal.emit(**self.kwargs) + + def __eq__(self, other): + return (self.signal == other.signal and self.kwargs == other.kwargs) + + def __str__(self): + return '%s: %s' % (self.signal.__class__.__name__, self.kwargs) diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/test.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/test.py new file mode 100644 index 000000000..c35640872 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/contrib/task/test.py @@ -0,0 +1,185 @@ +from __future__ import absolute_import +import pytest +import mock +import logging +import eventlet +import time +from signalslot import Signal +from signalslot.contrib.task import Task + +eventlet.monkey_patch(time=True) + + +class TestTask(object): + def setup_method(self, method): + self.signal = mock.Mock() + + def get_task_mock(self, *methods, **kwargs): + if kwargs.get('logger'): + log = logging.getLogger('TestTask') + else: + log = None + task_mock = Task(self.signal, logger=log) + + for method in methods: + setattr(task_mock, method, mock.Mock()) + + return task_mock + + def test_eq(self): + x = Task(self.signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskX')) + y = Task(self.signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskY')) + + assert x == y + + def test_not_eq(self): + x = Task(self.signal, dict(some_kwarg='foo', + logger=logging.getLogger('TaskX'))) + y = Task(self.signal, dict(some_kwarg='bar', + logger=logging.getLogger('TaskY'))) + + assert x != y + + def test_unicode(self): + t = Task(self.signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskT')) + + assert str(t) == "Mock: {'some_kwarg': 'foo'}" + + def test_get_or_create_gets(self): + x = Task.get_or_create(self.signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskX')) + y = Task.get_or_create(self.signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskY')) + + assert x is y + + def test_get_or_create_creates(self): + x = Task.get_or_create(self.signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskX')) + y = Task.get_or_create(self.signal, dict(some_kwarg='bar'), + logger=logging.getLogger('TaskY')) + + assert x is not y + + def test_get_or_create_without_kwargs(self): + t = Task.get_or_create(self.signal) + + assert t.kwargs == {} + + def test_get_or_create_uses_cls(self): + class Foo(Task): + pass + + assert isinstance(Foo.get_or_create(self.signal), Foo) + + def test_do_emit(self): + task_mock = self.get_task_mock('_clean', '_exception', '_completed') + + task_mock._do() + + self.signal.emit.assert_called_once_with() + + def test_do_emit_nolog(self): + task_mock = self.get_task_mock( + '_clean', '_exception', '_completed', logging=True) + + task_mock._do() + + self.signal.emit.assert_called_once_with() + + def test_do_emit_no_log(self): + task_mock = self.get_task_mock('_clean', '_exception', '_completed') + + task_mock._do() + + self.signal.emit.assert_called_once_with() + + def test_do_complete(self): + task_mock = self.get_task_mock('_clean', '_exception', '_completed') + + task_mock._do() + + task_mock._exception.assert_not_called() + task_mock._completed.assert_called_once_with() + task_mock._clean.assert_called_once_with() + + def test_do_success(self): + task_mock = self.get_task_mock() + assert task_mock._do() is True + + def test_do_failure_nolog(self): + # Our dummy exception + class DummyError(Exception): + pass + + task_mock = self.get_task_mock('_emit') + task_mock._emit.side_effect = DummyError() + + # This will throw an exception at us, be ready to catch it. + try: + task_mock._do() + assert False + except DummyError: + pass + + def test_do_failure_withlog(self): + task_mock = self.get_task_mock('_emit', logger=True) + task_mock._emit.side_effect = Exception() + assert task_mock._do() is False + + def test_do_exception(self): + task_mock = self.get_task_mock( + '_clean', '_exception', '_completed', '_emit') + + task_mock._emit.side_effect = Exception() + + task_mock._do() + + task_mock._exception.assert_called_once_with( + Exception, task_mock._emit.side_effect, mock.ANY) + + task_mock._completed.assert_not_called() + task_mock._clean.assert_called_once_with() + + @mock.patch('signalslot.signal.inspect') + def test_semaphore(self, inspect): + slot = mock.Mock() + slot.side_effect = lambda **k: time.sleep(.3) + + signal = Signal('tost') + signal.connect(slot) + + x = Task.get_or_create(signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskX')) + y = Task.get_or_create(signal, dict(some_kwarg='foo'), + logger=logging.getLogger('TaskY')) + + eventlet.spawn(x) + time.sleep(.1) + eventlet.spawn(y) + time.sleep(.1) + + assert slot.call_count == 1 + time.sleep(.4) + assert slot.call_count == 2 + + def test_call_context(self): + task_mock = self.get_task_mock('_clean', '_exception', '_completed', + '_emit') + + task_mock._emit.side_effect = Exception() + + assert task_mock.failures == 0 + task_mock() + assert task_mock.failures == 1 + + def test_call_success(self): + task_mock = self.get_task_mock('_clean', '_exception', '_completed', + '_emit') + + assert task_mock.failures == 0 + task_mock() + assert task_mock.failures == 0 diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/exceptions.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/exceptions.py new file mode 100644 index 000000000..272b1b347 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/exceptions.py @@ -0,0 +1,28 @@ +class SignalSlotException(Exception): + """Base class for all exceptions of this module.""" + pass + + +class SlotMustAcceptKeywords(SignalSlotException): + """ + Raised when connecting a slot that does not accept ``**kwargs`` in its + signature. + """ + def __init__(self, signal, slot): + m = 'Cannot connect %s to %s because it does not accept **kwargs' % ( + slot, signal) + + super(SlotMustAcceptKeywords, self).__init__(m) + + +# Not yet being used. +class QueueCantQueueNonSignalInstance(SignalSlotException): # pragma: no cover + """ + Raised when trying to queue something else than a + :py:class:`~signalslot.signal.Signal` instance. + """ + def __init__(self, queue, arg): + m = 'Cannot queue %s to %s because it is not a Signal instance' % ( + arg, queue) + + super(QueueCantQueueNonSignalInstance, self).__init__(m) diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/signal.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/signal.py new file mode 100644 index 000000000..435461ee0 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/signal.py @@ -0,0 +1,176 @@ +""" +Module defining the Signal class. +""" + +from __future__ import absolute_import +import inspect +import threading + +from . import exceptions + + +class DummyLock(object): + """ + Class that implements a no-op instead of a re-entrant lock. + """ + + def __enter__(self): + pass + + def __exit__(self, exc_type=None, exc_value=None, traceback=None): + pass + + +class BaseSlot(object): + """ + Slot abstract class for type resolution purposes. + """ + pass + + +class Signal(object): + """ + Define a signal by instanciating a :py:class:`Signal` object, ie.: + + >>> conf_pre_load = Signal() + + Optionaly, you can declare a list of argument names for this signal, ie.: + + >>> conf_pre_load = Signal(args=['conf']) + + Any callable can be connected to a Signal, it **must** accept keywords + (``**kwargs``), ie.: + + >>> def yourmodule_conf(conf, **kwargs): + ... conf['yourmodule_option'] = 'foo' + ... + + Connect your function to the signal using :py:meth:`connect`: + + >>> conf_pre_load.connect(yourmodule_conf) + + Emit the signal to call all connected callbacks using + :py:meth:`emit`: + + >>> conf = {} + >>> conf_pre_load.emit(conf=conf) + >>> conf + {'yourmodule_option': 'foo'} + + Note that you may disconnect a callback from a signal if it is already + connected: + + >>> conf_pre_load.is_connected(yourmodule_conf) + True + >>> conf_pre_load.disconnect(yourmodule_conf) + >>> conf_pre_load.is_connected(yourmodule_conf) + False + """ + def __init__(self, args=None, name=None, threadsafe=False): + self._slots = [] + self._slots_lk = threading.RLock() if threadsafe else DummyLock() + self.args = args or [] + self.name = name + + @property + def slots(self): + """ + Return a list of slots for this signal. + """ + with self._slots_lk: + # Do a slot clean-up + slots = [] + for s in self._slots: + if isinstance(s, BaseSlot) and (not s.is_alive): + continue + slots.append(s) + self._slots = slots + return list(slots) + + def connect(self, slot): + """ + Connect a callback ``slot`` to this signal. + """ + if not isinstance(slot, BaseSlot): + if getattr(inspect, "getfullargspec", False): + try: + if inspect.getfullargspec(slot).varkw is None: + raise exceptions.SlotMustAcceptKeywords(self, slot) + except TypeError: + if inspect.getfullargspec(slot.__call__).varkw is None: + raise exceptions.SlotMustAcceptKeywords(self, slot) + else: + try: + if inspect.getargspec(slot).keywords is None: + raise exceptions.SlotMustAcceptKeywords(self, slot) + except TypeError: + if inspect.getargspec(slot.__call__).keywords is None: + raise exceptions.SlotMustAcceptKeywords(self, slot) + + with self._slots_lk: + if not self.is_connected(slot): + self._slots.append(slot) + + def is_connected(self, slot): + """ + Check if a callback ``slot`` is connected to this signal. + """ + with self._slots_lk: + return slot in self._slots + + def disconnect(self, slot): + """ + Disconnect a slot from a signal if it is connected else do nothing. + """ + with self._slots_lk: + if self.is_connected(slot): + self._slots.pop(self._slots.index(slot)) + + def emit(self, **kwargs): + """ + Emit this signal which will execute every connected callback ``slot``, + passing keyword arguments. + + If a slot returns anything other than None, then :py:meth:`emit` will + return that value preventing any other slot from being called. + + >>> need_something = Signal() + >>> def get_something(**kwargs): + ... return 'got something' + ... + >>> def make_something(**kwargs): + ... print('I will not be called') + ... + >>> need_something.connect(get_something) + >>> need_something.connect(make_something) + >>> need_something.emit() + 'got something' + """ + for slot in reversed(self.slots): + result = slot(**kwargs) + + if result is not None: + return result + + def __eq__(self, other): + """ + Return True if other has the same slots connected. + + >>> a = Signal() + >>> b = Signal() + >>> a == b + True + >>> def slot(**kwargs): + ... pass + ... + >>> a.connect(slot) + >>> a == b + False + >>> b.connect(slot) + >>> a == b + True + """ + return self.slots == other.slots + + def __repr__(self): + return '' % (self.name or 'NO_NAME') diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/slot.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/slot.py new file mode 100644 index 000000000..18363e796 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/slot.py @@ -0,0 +1,74 @@ +""" +Module defining the Slot class. +""" + +from __future__ import absolute_import +import types +import weakref +import sys + +from .signal import BaseSlot + +# We cannot test a branch for Python >= 3.4 in Python < 3.4. +if sys.version_info < (3, 4): # pragma: no cover + from weakrefmethod import WeakMethod +else: # pragma: no cover + from weakref import WeakMethod + + +class Slot(BaseSlot): + """ + A slot is a callable object that manages a connection to a signal. + If weak is true or the slot is a subclass of weakref.ref, the slot + is automatically de-referenced to the called function. + """ + def __init__(self, slot, weak=False): + self._weak = weak or isinstance(slot, weakref.ref) + if weak and not isinstance(slot, weakref.ref): + if isinstance(slot, types.MethodType): + slot = WeakMethod(slot) + else: + slot = weakref.ref(slot) + self._slot = slot + + @property + def is_alive(self): + """ + Return True if this slot is "alive". + """ + return (not self._weak) or (self._slot() is not None) + + @property + def func(self): + """ + Return the function that is called by this slot. + """ + if self._weak: + return self._slot() + else: + return self._slot + + def __call__(self, **kwargs): + """ + Execute this slot. + """ + func = self.func + if func is not None: + return func(**kwargs) + + def __eq__(self, other): + """ + Compare this slot to another. + """ + if isinstance(other, BaseSlot): + return self.func == other.func + else: + return self.func == other + + def __repr__(self): + fn = self.func + if fn is None: + fn = 'dead' + else: + fn = repr(fn) + return '' % fn diff --git a/script.plexmod/lib/_included_packages/plexnet/signalslot/tests.py b/script.plexmod/lib/_included_packages/plexnet/signalslot/tests.py new file mode 100644 index 000000000..87ff3dce8 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalslot/tests.py @@ -0,0 +1,206 @@ +from __future__ import absolute_import +import pytest +import mock + +from signalslot import Signal, SlotMustAcceptKeywords, Slot + + +@mock.patch('signalslot.signal.inspect') +class TestSignal(object): + def setup_method(self, method): + self.signal_a = Signal(threadsafe=True) + self.signal_b = Signal(args=['foo']) + + self.slot_a = mock.Mock(spec=lambda **kwargs: None) + self.slot_a.return_value = None + self.slot_b = mock.Mock(spec=lambda **kwargs: None) + self.slot_b.return_value = None + + def test_is_connected(self, inspect): + self.signal_a.connect(self.slot_a) + + assert self.signal_a.is_connected(self.slot_a) + assert not self.signal_a.is_connected(self.slot_b) + assert not self.signal_b.is_connected(self.slot_a) + assert not self.signal_b.is_connected(self.slot_b) + + def test_emit_one_slot(self, inspect): + self.signal_a.connect(self.slot_a) + + self.signal_a.emit() + + self.slot_a.assert_called_once_with() + assert self.slot_b.call_count == 0 + + def test_emit_two_slots(self, inspect): + self.signal_a.connect(self.slot_a) + self.signal_a.connect(self.slot_b) + + self.signal_a.emit() + + self.slot_a.assert_called_once_with() + self.slot_b.assert_called_once_with() + + def test_emit_one_slot_with_arguments(self, inspect): + self.signal_b.connect(self.slot_a) + + self.signal_b.emit(foo='bar') + + self.slot_a.assert_called_once_with(foo='bar') + assert self.slot_b.call_count == 0 + + def test_emit_two_slots_with_arguments(self, inspect): + self.signal_b.connect(self.slot_a) + self.signal_b.connect(self.slot_b) + + self.signal_b.emit(foo='bar') + + self.slot_a.assert_called_once_with(foo='bar') + self.slot_b.assert_called_once_with(foo='bar') + + def test_reconnect_does_not_duplicate(self, inspect): + self.signal_a.connect(self.slot_a) + self.signal_a.connect(self.slot_a) + self.signal_a.emit() + + self.slot_a.assert_called_once_with() + + def test_disconnect_does_not_fail_on_not_connected_slot(self, inspect): + self.signal_a.disconnect(self.slot_b) + + +def test_anonymous_signal_has_nice_repr(): + signal = Signal() + assert repr(signal) == '' + + +def test_named_signal_has_a_nice_repr(): + signal = Signal(name='update_stuff') + assert repr(signal) == '' + + +class TestSignalConnect(object): + def setup_method(self, method): + self.signal = Signal() + + def test_connect_with_kwargs(self): + def cb(**kwargs): + pass + + self.signal.connect(cb) + + def test_connect_without_kwargs(self): + def cb(): + pass + + with pytest.raises(SlotMustAcceptKeywords): + self.signal.connect(cb) + + +class MyTestError(Exception): + pass + + +class TestException(object): + def setup_method(self, method): + self.signal = Signal(threadsafe=False) + self.seen_exception = False + + def failing_slot(**args): + raise MyTestError('die!') + + self.signal.connect(failing_slot) + + def test_emit_exception(self): + try: + self.signal.emit() + except MyTestError: + self.seen_exception = True + + assert self.seen_exception + + +class TestStrongSlot(object): + def setup_method(self, method): + self.called = False + + def slot(**kwargs): + self.called = True + + self.slot = Slot(slot) + + def test_alive(self): + assert self.slot.is_alive + + def test_call(self): + self.slot(testing=1234) + assert self.called + + +class TestWeakFuncSlot(object): + def setup_method(self, method): + self.called = False + + def slot(**kwargs): + self.called = True + + self.slot = Slot(slot, weak=True) + self.slot_ref = slot + + def test_alive(self): + assert self.slot.is_alive + assert repr(self.slot) == '' % repr(self.slot_ref) + + def test_call(self): + self.slot(testing=1234) + assert self.called + + def test_gc(self): + self.slot_ref = None + assert not self.slot.is_alive + assert repr(self.slot) == '' + self.slot(testing=1234) + + +class TestWeakMethodSlot(object): + def setup_method(self, method): + + class MyObject(object): + + def __init__(self): + self.called = False + + def slot(self, **kwargs): + self.called = True + + self.obj_ref = MyObject() + self.slot = Slot(self.obj_ref.slot, weak=True) + self.signal = Signal() + self.signal.connect(self.slot) + + def test_alive(self): + assert self.slot.is_alive + + def test_call(self): + self.signal.emit(testing=1234) + assert self.obj_ref.called + + def test_gc(self): + self.obj_ref = None + assert not self.slot.is_alive + self.signal.emit(testing=1234) + + +class TestSlotEq(object): + def setup_method(self, method): + self.slot_a = Slot(self.slot, weak=False) + self.slot_b = Slot(self.slot, weak=True) + + def slot(self, **kwargs): + pass + + def test_eq_other(self): + assert self.slot_a == self.slot_b + + def test_eq_func(self): + assert self.slot_a == self.slot diff --git a/script.plexmod/lib/_included_packages/plexnet/signalsmixin.py b/script.plexmod/lib/_included_packages/plexnet/signalsmixin.py new file mode 100644 index 000000000..2efe9d6e6 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/signalsmixin.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import +from . import signalslot + + +class SignalsMixin(object): + def __init__(self): + self._signals = {} + + def on(self, signalName, callback): + if signalName not in self._signals: + self._signals[signalName] = signalslot.Signal(threadsafe=True) + + signal = self._signals[signalName] + + signal.connect(callback) + + def off(self, signalName, callback): + if not self._signals: + return + + if not signalName: + if not callback: + self._signals = {} + else: + for name in self._signals: + self.off(name, callback) + else: + if not callback: + if signalName in self._signals: + del self._signals[signalName] + else: + self._signals[signalName].disconnect(callback) + + def trigger(self, signalName, **kwargs): + if not self._signals: + return + + if signalName not in self._signals: + return + + self._signals[signalName].emit(**kwargs) diff --git a/script.plexmod/lib/_included_packages/plexnet/simpleobjects.py b/script.plexmod/lib/_included_packages/plexnet/simpleobjects.py new file mode 100644 index 000000000..16b7b8a78 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/simpleobjects.py @@ -0,0 +1,21 @@ +class Res(tuple): + def __str__(self): + return '{0}x{1}'.format(*self[:2]) + + @classmethod + def fromString(cls, res_string): + try: + return cls([int(n) for n in res_string.split('x')]) + except: + return None + + +class AttributeDict(dict): + def __getattr__(self, attr): + return self.get(attr) + + def __setattr__(self, attr, value): + self[attr] = value + + def __repr__(self): + return '<{0}:{1}:{2}>'.format(self.__class__.__name__, self.id, self.get('title', 'None').encode('utf8')) diff --git a/script.plexmod/lib/_included_packages/plexnet/threadutils.py b/script.plexmod/lib/_included_packages/plexnet/threadutils.py new file mode 100644 index 000000000..fda46b4de --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/threadutils.py @@ -0,0 +1,94 @@ +# import inspect +# import ctypes +from __future__ import absolute_import +import threading +# import time + + +# def _async_raise(tid, exctype): +# '''Raises an exception in the threads with id tid''' +# if not inspect.isclass(exctype): +# raise TypeError("Only types can be raised (not instances)") + +# try: +# res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype)) +# except AttributeError: +# # To catch: undefined symbol: PyThreadState_SetAsyncExc +# return + +# if res == 0: +# raise ValueError("invalid thread id") +# elif res != 1: +# # "if it returns a number greater than one, you're in trouble, +# # and you should call it again with exc=NULL to revert the effect" +# ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0) +# raise SystemError("PyThreadState_SetAsyncExc failed") + + +# class KillThreadException(Exception): +# pass + + +class KillableThread(threading.Thread): + pass + '''A thread class that supports raising exception in the thread from + another thread. + ''' + # def _get_my_tid(self): + # """determines this (self's) thread id + + # CAREFUL : this function is executed in the context of the caller + # thread, to get the identity of the thread represented by this + # instance. + # """ + # if not self.is_alive(): + # raise threading.ThreadError("the thread is not active") + + # return self.ident + + # def _raiseExc(self, exctype): + # """Raises the given exception type in the context of this thread. + + # If the thread is busy in a system call (time.sleep(), + # socket.accept(), ...), the exception is simply ignored. + + # If you are sure that your exception should terminate the thread, + # one way to ensure that it works is: + + # t = ThreadWithExc( ... ) + # ... + # t.raiseExc( SomeException ) + # while t.is_alive(): + # time.sleep( 0.1 ) + # t.raiseExc( SomeException ) + + # If the exception is to be caught by the thread, you need a way to + # check that your thread has caught it. + + # CAREFUL : this function is executed in the context of the + # caller thread, to raise an excpetion in the context of the + # thread represented by this instance. + # """ + # _async_raise(self._get_my_tid(), exctype) + + def kill(self, force_and_wait=False): + pass + # try: + # self._raiseExc(KillThreadException) + + # if force_and_wait: + # time.sleep(0.1) + # while self.is_alive(): + # self._raiseExc(KillThreadException) + # time.sleep(0.1) + # except threading.ThreadError: + # pass + + # def onKilled(self): + # pass + + # def run(self): + # try: + # self._Thread__target(*self._Thread__args, **self._Thread__kwargs) + # except KillThreadException: + # self.onKilled() diff --git a/script.plexmod/lib/_included_packages/plexnet/util.py b/script.plexmod/lib/_included_packages/plexnet/util.py new file mode 100644 index 000000000..031b36d49 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/util.py @@ -0,0 +1,311 @@ +from __future__ import absolute_import +from . import simpleobjects +import re +import sys +import time +import platform +import uuid +import threading +import six +from kodi_six import xbmcaddon + +from . import verlib +from . import compat + +if six.PY2: + Event = threading._Event +else: + Event = threading.Event + +BASE_HEADERS = '' + +# to maintain py2 compatibility, duplicate ADDON from lib.util to avoid circular import +ADDON = xbmcaddon.Addon() + + +def resetBaseHeaders(): + return { + 'X-Plex-Platform': X_PLEX_PLATFORM, + 'X-Plex-Platform-Version': X_PLEX_PLATFORM_VERSION, + 'X-Plex-Provides': X_PLEX_PROVIDES, + 'X-Plex-Product': "PM4K", + 'X-Plex-Version': ADDON.getAddonInfo('version'), + 'X-Plex-Device': X_PLEX_DEVICE, + 'X-Plex-Client-Identifier': X_PLEX_IDENTIFIER, + 'Accept-Encoding': 'gzip,deflate', + 'Accept-Language': ACCEPT_LANGUAGE, + 'User-Agent': '{0}/{1}'.format("PM4K", ADDON.getAddonInfo('version')) + } + + +# Core Settings +PROJECT = 'PlexNet' # name provided to plex server +VERSION = '0.0.0a1' # version of this api +TIMEOUT = 10 # request timeout +LONG_TIMEOUT = 20 # s +CONN_CHECK_TIMEOUT = 2.5 # s +LAN_REACHABILITY_TIMEOUT = 10 # ms +CHECK_LOCAL = False +LOCAL_OVER_SECURE = False +X_PLEX_CONTAINER_SIZE = 50 # max results to return in a single search page + +ACCEPT_LANGUAGE = 'en-US,en' + +# Plex Header Configuation +X_PLEX_PROVIDES = 'player,controller' # one or more of [player, controller, server] +X_PLEX_PLATFORM = platform.uname()[0] # Platform name, eg iOS, MacOSX, Android, LG, etc +X_PLEX_PLATFORM_VERSION = platform.uname()[2] # Operating system version, eg 4.3.1, 10.6.7, 3.2 +X_PLEX_PRODUCT = PROJECT # Plex application name, eg Laika, Plex Media Server, Media Link +X_PLEX_VERSION = VERSION # Plex application version number +USER_AGENT = '{0}/{1}'.format(PROJECT, VERSION) + +INTERFACE = None +TIMER = None +APP = None +MANAGER = None + +try: + _platform = platform.system() +except: + try: + _platform = platform.platform(terse=True) + except: + _platform = sys.platform + +X_PLEX_DEVICE = _platform # Device name and model number, eg iPhone3,2, Motorola XOOM, LG5200TV +X_PLEX_IDENTIFIER = str(hex(uuid.getnode())) # UUID, serial number, or other number unique per device + +BASE_HEADERS = resetBaseHeaders() + +QUALITY_LOCAL = 0 +QUALITY_REMOTE = 1 +QUALITY_ONLINE = 2 + +Res = simpleobjects.Res +AttributeDict = simpleobjects.AttributeDict + + +def setInterface(interface): + global INTERFACE + INTERFACE = interface + + +def setTimer(timer): + global TIMER + TIMER = timer + + +def setApp(app): + global APP + APP = app + + +def LOG(msg): + INTERFACE.LOG(msg) + + +def DEBUG_LOG(msg): + INTERFACE.DEBUG_LOG(msg) + + +def ERROR_LOG(msg): + INTERFACE.ERROR_LOG(msg) + + +def WARN_LOG(msg): + INTERFACE.WARN_LOG(msg) + + +def ERROR(msg=None, err=None): + INTERFACE.ERROR(msg, err) + + +def FATAL(msg=None): + INTERFACE.FATAL(msg) + + +def TEST(msg): + INTERFACE.LOG(' ---TEST: {0}'.format(msg)) + + +def userAgent(): + return INTERFACE.getGlobal("userAgent") + + +def dummyTranslate(string): + return string + + +def hideToken(token): + # return 'X' * len(token) + if not token: + return token + return '****' + token[-4:] + + +def cleanToken(url): + return re.sub('X-Plex-Token=[^&]+', 'X-Plex-Token=****', url) + + +def now(local=False): + if local: + return time.time() + else: + return time.mktime(time.gmtime()) + + +def joinArgs(args, includeQuestion=True): + if not args: + return '' + + arglist = [] + for key in sorted(args, key=lambda x: x.lower()): + value = str(args[key]) + arglist.append('{0}={1}'.format(key, compat.quote(value))) + + return '{0}{1}'.format(includeQuestion and '?' or '&', '&'.join(arglist)) + + +def addPlexHeaders(transferObj, token=None): + headers = {"X-Plex-Platform": INTERFACE.getGlobal("platform"), + "X-Plex-Version": INTERFACE.getGlobal("appVersionStr"), + "X-Plex-Client-Identifier": INTERFACE.getGlobal("clientIdentifier"), + "X-Plex-Platform-Version": INTERFACE.getGlobal("platformVersion", "unknown"), + "X-Plex-Product": INTERFACE.getGlobal("product"), + "X-Plex-Provides": not INTERFACE.getPreference("remotecontrol", False) and 'player' or '', + "X-Plex-Device": INTERFACE.getGlobal("device"), + "X-Plex-Model": INTERFACE.getGlobal("model"), + "X-Plex-Device-Name": INTERFACE.getGlobal("friendlyName"), + } + + transferObj.session.headers.update(headers) + + # Adding the X-Plex-Client-Capabilities header causes node.plexapp.com to 500 + if not type(transferObj) == "roUrlTransfer" or 'node.plexapp.com' not in transferObj.getUrl(): + transferObj.addHeader("X-Plex-Client-Capabilities", INTERFACE.getCapabilities()) + + addAccountHeaders(transferObj, token) + + +def addAccountHeaders(transferObj, token=None): + if token: + transferObj.addHeader("X-Plex-Token", token) + + # TODO(schuyler): Add username? + + +def validInt(int_str): + try: + return int(int_str) + except: + return 0 + + +def bitrateToString(bits): + if not bits: + return '' + + speed = bits / 1000000.0 + if speed < 1: + speed = int(round(bits / 1000.0)) + return '{0} Kbps'.format(speed) + else: + return '{0:.1f} Mbps'.format(speed) + + +def normalizedVersion(ver): + try: + modv = '.'.join(ver.split('.')[:4]).split('-', 1)[0] # Clean the version i.e. Turn 1.2.3.4-asdf8-ads7f into 1.2.3.4 + return verlib.NormalizedVersion(verlib.suggest_normalized_version(modv)) + except: + if ver: + ERROR() + return verlib.NormalizedVersion(verlib.suggest_normalized_version('0.0.0')) + + +class CompatEvent(Event): + def wait(self, timeout): + Event.wait(self, timeout) + return self.isSet() + + +class Timer(object): + def __init__(self, timeout, function, repeat=False, name=None, fname=None, *args, **kwargs): + self.function = function + self.timeout = timeout + self.repeat = repeat + self.args = args + self.kwargs = kwargs + self._reset = False + self.name = name or 'TIMER:{0}'.format(self.function) + self.fname = fname or repr(self.function) + self.event = CompatEvent() + self.start() + + def start(self): + self.event.clear() + self.thread = threading.Thread(target=self.run, name=self.name, *self.args, **self.kwargs) + self.thread.start() + + def run(self): + DEBUG_LOG('Timer {0}: {1}'.format(self.fname, self._reset and 'RESET'or 'STARTED')) + try: + while not self.event.isSet() and not self.shouldAbort(): + while not self.event.wait(self.timeout) and not self.shouldAbort(): + if self._reset: + return + + self.function(*self.args, **self.kwargs) + if not self.repeat: + return + finally: + if not self._reset: + if self in APP.timers: + APP.timers.remove(self) + + DEBUG_LOG('Timer {0}: FINISHED'.format(self.fname)) + + self._reset = False + + def cancel(self): + self.event.set() + + def reset(self): + self._reset = True + self.cancel() + if self.thread and self.thread.is_alive(): + self.thread.join() + self.start() + + def is_alive(self): + return self.thread and self.thread.is_alive() + + def shouldAbort(self): + return False + + def join(self): + if self.thread.is_alive(): + self.thread.join() + + def isExpired(self): + return self.event.isSet() + + +class RepeatingCounterTimer(Timer): + def __init__(self, timeout, function, repeat=True, *args, **kwargs): + self.ticks = 0 + self._function = function + super(RepeatingCounterTimer, self).__init__(timeout, self.count, repeat=repeat, + name='TIMER:{0}'.format(function), + fname=repr(function), *args, **kwargs) + + def count(self): + self.ticks += 1 + self._function(*self.args, **self.kwargs) + + def reset(self): + super(RepeatingCounterTimer, self).reset() + self.ticks = 0 + + +TIMER = Timer diff --git a/script.plexmod/lib/_included_packages/plexnet/verlib.py b/script.plexmod/lib/_included_packages/plexnet/verlib.py new file mode 100644 index 000000000..a06889031 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/verlib.py @@ -0,0 +1,329 @@ +""" +"Rational" version definition and parsing for DistutilsVersionFight +discussion at PyCon 2009. +""" +from __future__ import absolute_import +import re + + +class IrrationalVersionError(Exception): + """This is an irrational version.""" + pass + + +class HugeMajorVersionNumError(IrrationalVersionError): + """An irrational version because the major version number is huge + (often because a year or date was used). + + See `error_on_huge_major_num` option in `NormalizedVersion` for details. + This guard can be disabled by setting that option False. + """ + pass + +# A marker used in the second and third parts of the `parts` tuple, for +# versions that don't have those segments, to sort properly. An example +# of versions in sort order ('highest' last): +# 1.0b1 ((1,0), ('b',1), ('f',)) +# 1.0.dev345 ((1,0), ('f',), ('dev', 345)) +# 1.0 ((1,0), ('f',), ('f',)) +# 1.0.post256.dev345 ((1,0), ('f',), ('f', 'post', 256, 'dev', 345)) +# 1.0.post345 ((1,0), ('f',), ('f', 'post', 345, 'f')) +# ^ ^ ^ +# 'b' < 'f' ---------------------/ | | +# | | +# 'dev' < 'f' < 'post' -------------------/ | +# | +# 'dev' < 'f' ----------------------------------------------/ +# Other letters would do, but 'f' for 'final' is kind of nice. +FINAL_MARKER = ('f',) + +VERSION_RE = re.compile(r''' + ^ + (?P\d+\.\d+) # minimum 'N.N' + (?P(?:\.\d+)*) # any number of extra '.N' segments + (?: + (?P[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate + # 'rc'= alias for release candidate + (?P\d+(?:\.\d+)*) + )? + (?P(\.post(?P\d+))?(\.dev(?P\d+))?)? + $''', re.VERBOSE) + + +class NormalizedVersion(object): + """A rational version. + + Good: + 1.2 # equivalent to "1.2.0" + 1.2.0 + 1.2a1 + 1.2.3a2 + 1.2.3b1 + 1.2.3c1 + 1.2.3.4 + TODO: fill this out + + Bad: + 1 # mininum two numbers + 1.2a # release level must have a release serial + 1.2.3b + """ + + def __init__(self, s, error_on_huge_major_num=True): + """Create a NormalizedVersion instance from a version string. + + @param s {str} The version string. + @param error_on_huge_major_num {bool} Whether to consider an + apparent use of a year or full date as the major version number + an error. Default True. One of the observed patterns on PyPI before + the introduction of `NormalizedVersion` was version numbers like this: + 2009.01.03 + 20040603 + 2005.01 + This guard is here to strongly encourage the package author to + use an alternate version, because a release deployed into PyPI + and, e.g. downstream Linux package managers, will forever remove + the possibility of using a version number like "1.0" (i.e. + where the major number is less than that huge major number). + """ + self._parse(s, error_on_huge_major_num) + + @classmethod + def from_parts(cls, version, prerelease=FINAL_MARKER, + devpost=FINAL_MARKER): + return cls(cls.parts_to_str((version, prerelease, devpost))) + + def _parse(self, s, error_on_huge_major_num=True): + """Parses a string version into parts.""" + match = VERSION_RE.search(s) + if not match: + raise IrrationalVersionError(s) + + groups = match.groupdict() + parts = [] + + # main version + block = self._parse_numdots(groups['version'], s, False, 2) + extraversion = groups.get('extraversion') + if extraversion not in ('', None): + block += self._parse_numdots(extraversion[1:], s) + parts.append(tuple(block)) + + # prerelease + prerel = groups.get('prerel') + if prerel is not None: + block = [prerel] + block += self._parse_numdots(groups.get('prerelversion'), s, + pad_zeros_length=1) + parts.append(tuple(block)) + else: + parts.append(FINAL_MARKER) + + # postdev + if groups.get('postdev'): + post = groups.get('post') + dev = groups.get('dev') + postdev = [] + if post is not None: + postdev.extend([FINAL_MARKER[0], 'post', int(post)]) + if dev is None: + postdev.append(FINAL_MARKER[0]) + if dev is not None: + postdev.extend(['dev', int(dev)]) + parts.append(tuple(postdev)) + else: + parts.append(FINAL_MARKER) + self.parts = tuple(parts) + if error_on_huge_major_num and self.parts[0][0] > 1980: + raise HugeMajorVersionNumError("huge major version number, %r, " + "which might cause future problems: %r" % (self.parts[0][0], s)) + + def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True, + pad_zeros_length=0): + """Parse 'N.N.N' sequences, return a list of ints. + + @param s {str} 'N.N.N..." sequence to be parsed + @param full_ver_str {str} The full version string from which this + comes. Used for error strings. + @param drop_trailing_zeros {bool} Whether to drop trailing zeros + from the returned list. Default True. + @param pad_zeros_length {int} The length to which to pad the + returned list with zeros, if necessary. Default 0. + """ + nums = [] + for n in s.split("."): + if len(n) > 1 and n[0] == '0': + raise IrrationalVersionError("cannot have leading zero in " + "version number segment: '%s' in %r" % (n, full_ver_str)) + nums.append(int(n)) + if drop_trailing_zeros: + while nums and nums[-1] == 0: + nums.pop() + while len(nums) < pad_zeros_length: + nums.append(0) + return nums + + def __str__(self): + return self.parts_to_str(self.parts) + + @classmethod + def parts_to_str(cls, parts): + """Transforms a version expressed in tuple into its string + representation.""" + # XXX This doesn't check for invalid tuples + main, prerel, postdev = parts + s = '.'.join(str(v) for v in main) + if prerel is not FINAL_MARKER: + s += prerel[0] + s += '.'.join(str(v) for v in prerel[1:]) + if postdev and postdev is not FINAL_MARKER: + if postdev[0] == 'f': + postdev = postdev[1:] + i = 0 + while i < len(postdev): + if i % 2 == 0: + s += '.' + s += str(postdev[i]) + i += 1 + return s + + def __repr__(self): + return "%s('%s')" % (self.__class__.__name__, self) + + def _cannot_compare(self, other): + raise TypeError("cannot compare %s and %s" + % (type(self).__name__, type(other).__name__)) + + def __eq__(self, other): + if not isinstance(other, NormalizedVersion): + self._cannot_compare(other) + return self.parts == other.parts + + def __lt__(self, other): + if not isinstance(other, NormalizedVersion): + self._cannot_compare(other) + return self.parts < other.parts + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + return not (self.__lt__(other) or self.__eq__(other)) + + def __le__(self, other): + return self.__eq__(other) or self.__lt__(other) + + def __ge__(self, other): + return self.__eq__(other) or self.__gt__(other) + + +def suggest_normalized_version(s): + """Suggest a normalized version close to the given version string. + + If you have a version string that isn't rational (i.e. NormalizedVersion + doesn't like it) then you might be able to get an equivalent (or close) + rational version from this function. + + This does a number of simple normalizations to the given string, based + on observation of versions currently in use on PyPI. Given a dump of + those version during PyCon 2009, 4287 of them: + - 2312 (53.93%) match NormalizedVersion without change + - with the automatic suggestion + - 3474 (81.04%) match when using this suggestion method + + @param s {str} An irrational version string. + @returns A rational version string, or None, if couldn't determine one. + """ + try: + NormalizedVersion(s) + return s # already rational + except IrrationalVersionError: + pass + + rs = s.lower() + + # part of this could use maketrans + for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'), + ('beta', 'b'), ('rc', 'c'), ('-final', ''), + ('-pre', 'c'), + ('-release', ''), ('.release', ''), ('-stable', ''), + ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''), + ('final', '')): + rs = rs.replace(orig, repl) + + # if something ends with dev or pre, we add a 0 + rs = re.sub(r"pre$", r"pre0", rs) + rs = re.sub(r"dev$", r"dev0", rs) + + # if we have something like "b-2" or "a.2" at the end of the + # version, that is pobably beta, alpha, etc + # let's remove the dash or dot + rs = re.sub(r"([abc|rc])[\-\.](\d+)$", r"\1\2", rs) + + # 1.0-dev-r371 -> 1.0.dev371 + # 0.1-dev-r79 -> 0.1.dev79 + rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs) + + # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1 + rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs) + + # Clean: v0.3, v1.0 + if rs.startswith('v'): + rs = rs[1:] + + # Clean leading '0's on numbers. + # TODO: unintended side-effect on, e.g., "2003.05.09" + # PyPI stats: 77 (~2%) better + rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs) + + # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers + # zero. + # PyPI stats: 245 (7.56%) better + rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs) + + # the 'dev-rNNN' tag is a dev tag + rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs) + + # clean the - when used as a pre delimiter + rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs) + + # a terminal "dev" or "devel" can be changed into ".dev0" + rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs) + + # a terminal "dev" can be changed into ".dev0" + rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs) + + # a terminal "final" or "stable" can be removed + rs = re.sub(r"(final|stable)$", "", rs) + + # The 'r' and the '-' tags are post release tags + # 0.4a1.r10 -> 0.4a1.post10 + # 0.9.33-17222 -> 0.9.3.post17222 + # 0.9.33-r17222 -> 0.9.3.post17222 + rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs) + + # Clean 'r' instead of 'dev' usage: + # 0.9.33+r17222 -> 0.9.3.dev17222 + # 1.0dev123 -> 1.0.dev123 + # 1.0.git123 -> 1.0.dev123 + # 1.0.bzr123 -> 1.0.dev123 + # 0.1a0dev.123 -> 0.1a0.dev123 + # PyPI stats: ~150 (~4%) better + rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs) + + # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage: + # 0.2.pre1 -> 0.2c1 + # 0.2-c1 -> 0.2c1 + # 1.0preview123 -> 1.0c123 + # PyPI stats: ~21 (0.62%) better + rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs) + + # Tcl/Tk uses "px" for their post release markers + rs = re.sub(r"p(\d+)$", r".post\1", rs) + + try: + NormalizedVersion(rs) + return rs # already rational + except IrrationalVersionError: + pass + return None diff --git a/script.plexmod/lib/_included_packages/plexnet/video.py b/script.plexmod/lib/_included_packages/plexnet/video.py new file mode 100644 index 000000000..8adae39f7 --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/video.py @@ -0,0 +1,782 @@ +from __future__ import absolute_import +from functools import wraps + +from . import plexobjects +from . import media +from . import plexmedia +from . import plexstream +from . import exceptions +from . import compat +from . import plexlibrary +from . import util +from . import mediachoice + + +class PlexVideoItemList(plexobjects.PlexItemList): + def __init__(self, data, initpath=None, server=None, container=None): + self._data = data + self._initpath = initpath + self._server = server + self._container = container + self._items = None + + @property + def items(self): + if self._items is None: + if self._data is not None: + self._items = [plexobjects.buildItem(self._server, elem, self._initpath, container=self._container) for elem in self._data] + else: + self._items = [] + + return self._items + + +def forceMediaChoice(method): + @wraps(method) + def _impl(self, *method_args, **method_kwargs): + # set mediaChoice if we don't have any yet, or the one we have is incomplete and the new one isn't + media = method_kwargs.get("media", self.media()[0]) + partIndex = method_kwargs.get("partIndex", 0) + if not self.mediaChoice or (not self.mediaChoice.media.hasStreams() and media.hasStreams()): + self.setMediaChoice(media=media, partIndex=partIndex) + return method(self, *method_args, **method_kwargs) + return _impl + + +class Video(media.MediaItem): + TYPE = None + manually_selected_sub_stream = False + current_subtitle_is_embedded = False + _current_subtitle_idx = None + + def __init__(self, *args, **kwargs): + self._settings = None + media.MediaItem.__init__(self, *args, **kwargs) + + def __eq__(self, other): + return other and self.ratingKey == other.ratingKey + + def __ne__(self, other): + return not self.__eq__(other) + + @property + def settings(self): + if not self._settings: + from . import plexapp + self._settings = plexapp.PlayerSettingsInterface() + + return self._settings + + @settings.setter + def settings(self, value): + self._settings = value + + # overridden by Movie/Episode + @property + def subtitleStreams(self): + return [] + + @property + def videoStreams(self): + return [] + + @property + def audioStreams(self): + return [] + + def selectedVideoStream(self, fallback=False): + if self.videoStreams: + for stream in self.videoStreams: + if stream.isSelected(): + return stream + if fallback: + return self.videoStreams[0] + return None + + def selectedAudioStream(self, fallback=False): + if self.audioStreams: + for stream in self.audioStreams: + if stream.isSelected(): + return stream + if fallback: + return self.audioStreams[0] + return None + + def selectedSubtitleStream(self, forced_subtitles_override=False, fallback=False): + if self._current_subtitle_idx: + try: + return self.subtitleStreams[self._current_subtitle_idx] + except IndexError: + pass + + if self.subtitleStreams: + for stream in self.subtitleStreams: + if stream.isSelected(): + if forced_subtitles_override and \ + stream.forced.asBool() and self.manually_selected_sub_stream != stream.id: + # try finding a non-forced variant of this stream + possible_alt = None + for alt_stream in self.subtitleStreams: + if alt_stream.language == stream.language and alt_stream != stream \ + and not alt_stream.forced.asBool(): + if possible_alt and not possible_alt.key and alt_stream.key: + possible_alt = alt_stream + break + if not possible_alt: + possible_alt = alt_stream + if possible_alt: + util.DEBUG_LOG("Selecting stream %s instead of %s" % + (possible_alt, stream)) + stream.setSelected(False) + possible_alt.setSelected(True) + self.current_subtitle_is_embedded = possible_alt.embedded + if self._current_subtitle_idx != possible_alt.typeIndex: + self._current_subtitle_idx = possible_alt.typeIndex + return possible_alt + + if self._current_subtitle_idx != stream.typeIndex: + self._current_subtitle_idx = stream.typeIndex + self.current_subtitle_is_embedded = stream.embedded + return stream + if fallback: + stream = self.subtitleStreams[0] + if self._current_subtitle_idx != stream.typeIndex: + self._current_subtitle_idx = stream.typeIndex + return stream + return None + + def setMediaChoice(self, media=None, partIndex=0): + media = media or self.media()[0] + self.mediaChoice = mediachoice.MediaChoice(media, partIndex=partIndex) + + @forceMediaChoice + def selectStream(self, stream, _async=True): + self.mediaChoice.part.setSelectedStream(stream.streamType.asInt(), stream.id, _async) + # Update any affected streams + if stream.streamType.asInt() == plexstream.PlexStream.TYPE_AUDIO: + for audioStream in self.audioStreams: + if audioStream.id == stream.id: + audioStream.setSelected(True) + elif audioStream.isSelected(): + audioStream.setSelected(False) + elif stream.streamType.asInt() == plexstream.PlexStream.TYPE_SUBTITLE: + self._current_subtitle_idx = None + self.current_subtitle_is_embedded = False + for subtitleStream in self.subtitleStreams: + if subtitleStream.id == stream.id: + subtitleStream.setSelected(True) + self.current_subtitle_is_embedded = subtitleStream.embedded + self._current_subtitle_idx = subtitleStream.typeIndex + elif subtitleStream.isSelected(): + subtitleStream.setSelected(False) + + @forceMediaChoice + def cycleSubtitles(self, forward=True): + amount = len(self.subtitleStreams) + if not amount: + return False + cur = self.selectedSubtitleStream() + if not cur: + # use fallback + stream = self.selectedSubtitleStream(fallback=True) + else: + # set next if we're not at the end of the list + if forward: + if cur.typeIndex < len(self.subtitleStreams) - 1: + stream = self.subtitleStreams[cur.typeIndex+1] + else: + stream = self.subtitleStreams[0] + else: + if cur.typeIndex > 0: + stream = self.subtitleStreams[cur.typeIndex - 1] + else: + stream = self.subtitleStreams[-1] + + util.DEBUG_LOG("Selecting subtitle stream: {} (was: {})".format(stream, cur)) + self.selectStream(stream) + return stream + + @forceMediaChoice + def disableSubtitles(self): + self.selectStream(plexstream.NONE_STREAM) + + @property + def hasSubtitle(self): + return bool(self.selectedSubtitleStream()) + + @property + def hasSubtitles(self): + return bool(self.subtitleStreams) + + def isVideoItem(self): + return True + + @forceMediaChoice + def _findStreams(self, streamtype, withMC=True): + idx = 0 + streams = [] + source = [self.mediaChoice.media] if withMC else self.media() + for media_ in source: + parts = [self.mediaChoice.part] if withMC else media_.parts + for part in parts: + for stream in part.streams: + if stream.streamType.asInt() == streamtype: + stream.typeIndex = idx + streams.append(stream) + idx += 1 + return streams + + def analyze(self): + """ The primary purpose of media analysis is to gather information about that media + item. All of the media you add to a Library has properties that are useful to + know - whether it's a video file, a music track, or one of your photos. + """ + self.server.query('/%s/analyze' % self.key) + + def markWatched(self, **kwargs): + path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey + self.server.query(path) + self.reload(**kwargs) + + def markUnwatched(self, **kwargs): + path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey + self.server.query(path) + self.reload(**kwargs) + + # def play(self, client): + # client.playMedia(self) + + def refresh(self): + self.server.query('%s/refresh' % self.key, method=self.server.session.put) + + def _getStreamURL(self, **params): + if self.TYPE not in ('movie', 'episode', 'track'): + raise exceptions.Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE) + mvb = params.get('maxVideoBitrate') + vr = params.get('videoResolution') + + # import plexapp + + params = { + 'path': self.key, + 'offset': params.get('offset', 0), + 'copyts': params.get('copyts', 1), + 'protocol': params.get('protocol', 'hls'), + 'mediaIndex': params.get('mediaIndex', 0), + 'directStream': '1', + 'directPlay': '0', + 'X-Plex-Platform': params.get('platform', ''), + # 'X-Plex-Platform': params.get('platform', util.INTERFACE.getGlobal('platform')), + 'maxVideoBitrate': max(mvb, 64) if mvb else None, + 'videoResolution': '{0}x{1}'.format(*vr) if vr else None + } + + final = {} + + for k, v in params.items(): + if v is not None: # remove None values + final[k] = v + + streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video' + server = self.getTranscodeServer(True, self.TYPE) + + return server.buildUrl('/{0}/:/transcode/universal/start.m3u8?{1}'.format(streamtype, compat.urlencode(final)), includeToken=True) + # path = "/video/:/transcode/universal/" + command + "?session=" + AppSettings().GetGlobal("clientIdentifier") + + @forceMediaChoice + def resolutionString(self): + res = self.mediaChoice.media.videoResolution + if not res: + return '' + + if res.isdigit(): + return '{0}p'.format(self.mediaChoice.media.videoResolution) + else: + return res.upper() + + @forceMediaChoice + def audioCodecString(self): + codec = (self.mediaChoice.media.audioCodec or '').lower() + + if codec in ('dca', 'dca-ma', 'dts-hd', 'dts-es', 'dts-hra'): + codec = "DTS" + else: + codec = codec.upper() + + return codec + + @forceMediaChoice + def videoCodecString(self): + return (self.mediaChoice.media.videoCodec or '').upper() + + @property + @forceMediaChoice + def videoCodecRendering(self): + stream = self.mediaChoice.videoStream + + if not stream: + return '' + + return stream.videoCodecRendering + + @forceMediaChoice + def audioChannelsString(self, translate_func=util.dummyTranslate): + channels = self.mediaChoice.media.audioChannels.asInt() + + if channels == 1: + return translate_func("Mono") + elif channels == 2: + return translate_func("Stereo") + elif channels > 0: + return "{0}.1".format(channels - 1) + else: + return "" + + @property + def remainingTime(self): + if not self.viewOffset.asInt(): + return + return (self.duration.asInt() - self.viewOffset.asInt()) // 1000 + + @property + def remainingTimeString(self): + if not self.remainingTime: + return '' + seconds = self.remainingTime + hours = seconds // 3600 + minutes = (seconds - hours * 3600) // 60 + return (hours and "{}h ".format(hours) or '') + (minutes and "{}m".format(minutes) or "0m") + + def available(self): + return any(v.isAccessible() for v in self.media()) + + +class RelatedMixin(object): + _relatedCount = None + + @property + def relatedCount(self): + if self._relatedCount is None: + related = self.getRelated(0, 0) + if related is not None: + self._relatedCount = related.totalSize + else: + self._relatedCount = 0 + + return self._relatedCount + + @property + def related(self): + return self.getRelated(0, 8) + + def getRelated(self, offset=None, limit=None, _max=36): + path = '/library/metadata/%s/similar' % self.ratingKey + try: + return plexobjects.listItems(self.server, path, offset=offset, limit=limit, params={"count": _max}) + except exceptions.BadRequest: + util.DEBUG_LOG("Invalid related items response returned for %s" % self) + return None + + +class SectionOnDeckMixin(object): + _sectionOnDeckCount = None + + def sectionOnDeck(self, offset=None, limit=None): + query = '/library/sections/{0}/onDeck'.format(self.getLibrarySectionId()) + return plexobjects.listItems(self.server, query, offset=offset, limit=limit) + + @property + def sectionOnDeckCount(self): + if self._sectionOnDeckCount is None: + self._sectionOnDeckCount = self.sectionOnDeck(0, 0).totalSize + + return self._sectionOnDeckCount + + +class PlayableVideo(Video, RelatedMixin): + TYPE = None + _videoStreams = None + _audioStreams = None + _subtitleStreams = None + _current_subtitle_idx = None + + def _setData(self, data): + Video._setData(self, data) + if self.isFullObject(): + self.extras = PlexVideoItemList(data.find('Extras'), initpath=self.initpath, server=self.server, container=self) + self.chapters = plexobjects.PlexItemList(data, media.Chapter, media.Chapter.TYPE, server=self.server) + + self.resetStreams() + + def setMediaChoice(self, *args, **kwargs): + """ + Reset cached streams after setting a mediaChoice + """ + super(PlayableVideo, self).setMediaChoice(*args, **kwargs) + self.resetStreams() + + def resetStreams(self): + self._videoStreams = None + self._audioStreams = None + self._subtitleStreams = None + + def reload(self, *args, **kwargs): + if not kwargs.get('_soft'): + if self.get('viewCount'): + del self.viewCount + if self.get('viewOffset'): + del self.viewOffset + + fromMediaChoice = kwargs.get("fromMediaChoice", False) + + # capture current IDs + mediaID = None + partID = None + streamIDs = None + reSelect = False + if fromMediaChoice and self.mediaChoice: + reSelect = True + mediaID = self.mediaChoice.media.id + partID = self.mediaChoice.part.id + streamIDs = [] + if self.mediaChoice.media.hasStreams(): + subtitleStream = self.selectedSubtitleStream(fallback=False) + streamIDs = [self.selectedVideoStream(fallback=True).id, + self.selectedAudioStream(fallback=True).id] + if subtitleStream: + streamIDs.append(subtitleStream.id) + + Video.reload(self, *args, **kwargs) + + # re-select selected IDs + if reSelect: + selMedia = None + selPartIndex = 0 + for media in self.media: + if media.id == mediaID: + selMedia = media + media.set('selected', '1') + for index, part in enumerate(media.parts): + if part.id == partID: + selPartIndex = index + for stream in part.streams: + if stream.id in streamIDs: + stream.setSelected(True) + self.mediaChoice = mediachoice.MediaChoice(selMedia, partIndex=selPartIndex) + + return self + + def postPlay(self, **params): + query = '/hubs/metadata/{0}/postplay'.format(self.ratingKey) + data = self.server.query(query, params=params) + container = plexobjects.PlexContainer(data, initpath=query, server=self.server, address=query) + + hubs = {} + + for elem in data: + hub = plexlibrary.Hub(elem, server=self.server, container=container) + hubs[hub.hubIdentifier] = hub + return hubs + + +@plexobjects.registerLibType +class Movie(PlayableVideo): + TYPE = 'movie' + + def _setData(self, data): + PlayableVideo._setData(self, data) + if self.isFullObject(): + self.collections = plexobjects.PlexItemList(data, media.Collection, media.Collection.TYPE, + server=self.server) + self.countries = plexobjects.PlexItemList(data, media.Country, media.Country.TYPE, server=self.server) + self.directors = plexobjects.PlexItemList(data, media.Director, media.Director.TYPE, server=self.server) + self.genres = plexobjects.PlexItemList(data, media.Genre, media.Genre.TYPE, server=self.server) + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, + initpath=self.initpath, server=self.server, media=self) + self.producers = plexobjects.PlexItemList(data, media.Producer, media.Producer.TYPE, server=self.server) + self.roles = plexobjects.PlexItemList(data, media.Role, media.Role.TYPE, server=self.server, + container=self.container) + self.reviews = plexobjects.PlexItemList(data, media.Review, media.Review.TYPE, server=self.server, + container=self.container) + self.writers = plexobjects.PlexItemList(data, media.Writer, media.Writer.TYPE, server=self.server) + #self.related = plexobjects.PlexItemList(data.find('Related'), plexlibrary.Hub, plexlibrary.Hub.TYPE, server=self.server, container=self) + else: + if data.find(media.Media.TYPE) is not None: + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self) + + self.markers = plexobjects.PlexItemList(data, media.Marker, media.Marker.TYPE, server=self.server) + + # data for active sessions + self.sessionKey = plexobjects.PlexValue(data.attrib.get('sessionKey', ''), self) + self.user = self._findUser(data) + self.player = self._findPlayer(data) + self.session = self._findSession(data) + self.transcodeSession = self._findTranscodeSession(data) + + @property + def defaultTitle(self): + title = self.title or '' + if self.editionTitle: + title = title + " \u2022 " + self.editionTitle + return title + + @property + def maxHeight(self): + height = 0 + for m in self.media: + if m.height.asInt() > height: + height = m.height.asInt() + return height + + @property + def videoStreams(self): + if self._videoStreams is None: + self._videoStreams = self._findStreams(plexstream.PlexStream.TYPE_VIDEO) + return self._videoStreams + + @property + def audioStreams(self): + if self._audioStreams is None: + self._audioStreams = self._findStreams(plexstream.PlexStream.TYPE_AUDIO) + return self._audioStreams + + @property + def subtitleStreams(self): + if self._subtitleStreams is None: + self._subtitleStreams = self._findStreams(plexstream.PlexStream.TYPE_SUBTITLE) + return self._subtitleStreams + + @property + def actors(self): + return self.roles + + @property + def isWatched(self): + return self.get('viewCount').asInt() > 0 or self.get('viewOffset').asInt() > 0 + + def getStreamURL(self, **params): + return self._getStreamURL(**params) + + +@plexobjects.registerLibType +class Show(Video, RelatedMixin, SectionOnDeckMixin): + TYPE = 'show' + + def _setData(self, data): + Video._setData(self, data) + if self.isFullObject(): + self.genres = plexobjects.PlexItemList(data, media.Genre, media.Genre.TYPE, server=self.server) + self.roles = plexobjects.PlexItemList(data, media.Role, media.Role.TYPE, server=self.server, container=self.container) + #self.related = plexobjects.PlexItemList(data.find('Related'), plexlibrary.Hub, plexlibrary.Hub.TYPE, server=self.server, container=self) + self.extras = PlexVideoItemList(data.find('Extras'), initpath=self.initpath, server=self.server, container=self) + + @property + def unViewedLeafCount(self): + return self.leafCount.asInt() - self.viewedLeafCount.asInt() + + @property + def isWatched(self): + return self.viewedLeafCount == self.leafCount + + @property + def playbackSettings(self): + return util.INTERFACE.playbackManager(self) + + def seasons(self): + path = self.key + return plexobjects.listItems(self.server, path, Season.TYPE) + + def season(self, title): + path = self.key + return plexobjects.findItem(self.server, path, title) + + def episodes(self, watched=None, offset=None, limit=None): + leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey + return plexobjects.listItems(self.server, leavesKey, watched=watched, offset=offset, limit=limit) + + def episode(self, title): + path = '/library/metadata/%s/allLeaves' % self.ratingKey + return plexobjects.findItem(self.server, path, title) + + def all(self): + return self.episodes() + + def watched(self): + return self.episodes(watched=True) + + def unwatched(self): + return self.episodes(watched=False) + + def refresh(self): + self.server.query('/library/metadata/%s/refresh' % self.ratingKey) + + +@plexobjects.registerLibType +class Season(Video): + TYPE = 'season' + + def _setData(self, data): + Video._setData(self, data) + if self.isFullObject(): + self.extras = PlexVideoItemList(data.find('Extras'), initpath=self.initpath, server=self.server, container=self) + + @property + def defaultTitle(self): + return self.parentTitle or self.title + + @property + def unViewedLeafCount(self): + return self.leafCount.asInt() - self.viewedLeafCount.asInt() + + @property + def isWatched(self): + return self.viewedLeafCount == self.leafCount + + def episodes(self, watched=None, offset=None, limit=None): + path = self.key + return plexobjects.listItems(self.server, path, watched=watched, offset=offset, limit=limit) + + def episode(self, title): + path = self.key + return plexobjects.findItem(self.server, path, title) + + def all(self): + return self.episodes() + + def show(self): + return plexobjects.listItems(self.server, self.parentKey)[0] + + def watched(self): + return self.episodes(watched=True) + + def unwatched(self): + return self.episodes(watched=False) + + +@plexobjects.registerLibType +class Episode(PlayableVideo, SectionOnDeckMixin): + TYPE = 'episode' + + def init(self, data): + self._show = None + self._season = None + + def _setData(self, data): + PlayableVideo._setData(self, data) + if self.isFullObject(): + self.directors = plexobjects.PlexItemList(data, media.Director, media.Director.TYPE, server=self.server) + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self) + self.writers = plexobjects.PlexItemList(data, media.Writer, media.Writer.TYPE, server=self.server) + else: + if data.find(media.Media.TYPE) is not None: + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self) + + self.markers = plexobjects.PlexItemList(data, media.Marker, media.Marker.TYPE, server=self.server) + + # data for active sessions + self.sessionKey = plexobjects.PlexValue(data.attrib.get('sessionKey', ''), self) + self.user = self._findUser(data) + self.player = self._findPlayer(data) + self.session = self._findSession(data) + self.transcodeSession = self._findTranscodeSession(data) + + @property + def defaultTitle(self): + return self.grandparentTitle or self.parentTitle or self.title + + @property + def defaultThumb(self): + return self.grandparentThumb or self.parentThumb or self.thumb + + @property + def videoStreams(self): + if self._videoStreams is None: + self._videoStreams = self._findStreams(plexstream.PlexStream.TYPE_VIDEO) + return self._videoStreams + + @property + def audioStreams(self): + if self._audioStreams is None: + self._audioStreams = self._findStreams(plexstream.PlexStream.TYPE_AUDIO) + return self._audioStreams + + @property + def subtitleStreams(self): + if self._subtitleStreams is None: + self._subtitleStreams = self._findStreams(plexstream.PlexStream.TYPE_SUBTITLE) + return self._subtitleStreams + + @property + def isWatched(self): + return self.get('viewCount').asInt() > 0 or self.get('viewOffset').asInt() > 0 + + @property + def playbackSettings(self): + return self.show().playbackSettings + + def getStreamURL(self, **params): + return self._getStreamURL(**params) + + def season(self): + skipParent = self.get('skipParent').asBool() + key = self.parentKey if not skipParent else self.grandparentKey + if not self._season: + items = plexobjects.listItems(self.server, key) + + if items: + self._season = items[0] + return self._season + + def show(self): + if not self._show: + self._show = plexobjects.listItems(self.server, self.grandparentKey)[0] + return self._show + + @property + def genres(self): + return self.show().genres + + @property + def roles(self): + return self.show().roles + + def getRelated(self, offset=None, limit=None, _max=36): + return self.show().getRelated(offset=offset, limit=limit, _max=_max) + + +@plexobjects.registerLibType +class Clip(PlayableVideo): + TYPE = 'clip' + + def _setData(self, data): + PlayableVideo._setData(self, data) + if self.isFullObject(): + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self) + else: + if data.find(media.Media.TYPE) is not None: + self.media = plexobjects.PlexMediaItemList(data, plexmedia.PlexMedia, media.Media.TYPE, initpath=self.initpath, server=self.server, media=self) + + @property + def isWatched(self): + return self.get('viewCount').asInt() > 0 or self.get('viewOffset').asInt() > 0 + + def getStreamURL(self, **params): + return self._getStreamURL(**params) + + @property + def videoStreams(self): + if self._videoStreams is None: + self._videoStreams = self._findStreams(plexstream.PlexStream.TYPE_VIDEO) + return self._videoStreams + + @property + def audioStreams(self): + if self._audioStreams is None: + self._audioStreams = self._findStreams(plexstream.PlexStream.TYPE_AUDIO) + return self._audioStreams + + @property + def subtitleStreams(self): + if self._subtitleStreams is None: + self._subtitleStreams = self._findStreams(plexstream.PlexStream.TYPE_SUBTITLE) + return self._subtitleStreams diff --git a/script.plexmod/lib/_included_packages/plexnet/videosession.py b/script.plexmod/lib/_included_packages/plexnet/videosession.py new file mode 100644 index 000000000..933dac0ed --- /dev/null +++ b/script.plexmod/lib/_included_packages/plexnet/videosession.py @@ -0,0 +1,416 @@ +# coding=utf-8 +import six +from collections import OrderedDict +from plexnet import plexapp + + +class MediaDetails: + """ + Gathers attributes from a MediaContainer + """ + details = None + attribute_map = { + "container": "part.attrib_container", + "partDecision": "part.decision", + "videoResolution": "media.videoResolution", + "videoBitrate": "video_stream.bitrate", + "videoCodec": "video_stream.codec", + "videoStreamDecision": "video_stream.decision", + "transcodeVideoDecision": "transcode_session.videoDecision", + "transcodeHWEncoding": "transcode_session.transcodeHwEncoding", + "audioCodec": "audio_stream.codec", + "audioBitrate": "audio_stream.bitrate", + "audioChannels": "audio_stream.channels", + "audioStreamDecision": "audio_stream.decision", + "subtitleCodec": "subtitle_stream.codec", + "subtitleStreamDecision": "subtitle_stream.decision", + "subtitleLocation": "subtitle_stream.location", + "subtitleBurn": "subtitle_stream.burn", + } + + def __init__(self, *args, **kwargs): + self.details = self.findMediaDetails(*args, **kwargs) + + def attributesFromInstance(self, map, reference_data): + # gather attribute values + final_data = {} + for attribute, dataPath in six.iteritems(map): + objName, attribName = dataPath.split(".") + if objName in reference_data: + obj = reference_data[objName] + final_data[attribute] = getattr(obj, attribName, None) or None + return final_data + + def findMediaDetails(self, mediaContainer, mediaChoice, transcodeSession=None): + """ + + """ + reference_data = { + "media": None, + "part": None, + "video_stream": None, + "audio_stream": None, + "subtitle_stream": None, + "transcode_session": transcodeSession or mediaContainer.transcodeSession + } + + # We can't use mediaChoice here directly, because it doesn't necessarily hold the newest data (in case of + # an actual MediaContainer from the Session endpoint that data is king) + # Instead find the media/part/streams which were selected and are held in MediaChoice inside the current + # mediaContainer + for media in mediaContainer.media: + if media.id == mediaChoice.media.id: + reference_data["media"] = media + + for part in media.parts: + if part.id == mediaChoice.part.id: + reference_data["part"] = part + + for stream in part.streams: + if mediaChoice.videoStream != None and stream.id == mediaChoice.videoStream.id: + reference_data["video_stream"] = stream + elif mediaChoice.audioStream != None and stream.id == mediaChoice.audioStream.id: + reference_data["audio_stream"] = stream + elif mediaChoice.subtitleStream != None and stream.id == mediaChoice.subtitleStream.id: + reference_data["subtitle_stream"] = stream + + final_data = { + "hasVideoStream": bool(reference_data["video_stream"]), + "hasAudioStream": bool(reference_data["audio_stream"]), + "hasSubtitleStream": bool(reference_data["subtitle_stream"]), + } + final_data.update(self.attributesFromInstance(self.attribute_map, reference_data)) + + del reference_data + + return final_data + + def __getattr__(self, item): + if self.details and item in self.details: + return self.details[item] + raise AttributeError("%r object has no attribute %r" % (self.__class__, item)) + + +class MediaDetailsIncomplete(MediaDetails): + """ + Gathers attributes from a TranscodeSession + """ + + incompleteAttribMap = { + "container": "transcode_session.attrib_container", + "videoCodec": "transcode_session.videoCodec", + "videoStreamDecision": "transcode_session.videoDecision", + "transcodeVideoDecision": "transcode_session.videoDecision", + "audioCodec": "transcode_session.audioCodec", + "audioChannels": "transcode_session.audioChannels", + "audioStreamDecision": "transcode_session.audioDecision", + "subtitleStreamDecision": "transcode_session.subtitleDecision", + } + + def findMediaDetails(self, mediaContainer, mediaChoice, incompleteSessionData=None): + transcodeSession = mediaContainer._findTranscodeSession(incompleteSessionData) + + # get base data from original mediaChoice media instance + data = MediaDetails.findMediaDetails(self, mediaContainer, mediaChoice, transcodeSession=transcodeSession) + + decision = "directplay" + bandwidths = mediaContainer._findBandwidths(incompleteSessionData) + + if transcodeSession: + decision = "transcode" + + data.update(self.attributesFromInstance(self.incompleteAttribMap, {"transcode_session": transcodeSession})) + + # fill remaining data + + for bw in bandwidths: + if bw.resolution: + data["videoResolution"] = bw.resolution + break + + if data["hasVideoStream"]: + # sadly we don't know the final bitrate for the video/audio streams with incomplete data + data["videoBitrate"] = "?" + + if data["hasAudioStream"]: + data["audioBitrate"] = "?" + + if data["hasSubtitleStream"]: + if data["subtitleStreamDecision"] == "burn": + data["subtitleBurn"] = True + data["subtitleCodec"] = "burn" + + data["partDecision"] = decision + + return data + + +class MediaDetailsHolder: + """ + Holds information about the currently selected MediaContainer (self.original) and the currently playing + MediaContainer (self.session) + """ + session = None + original = None + + def __init__(self, originalMedia, sessionMedia, mediaChoice, incompleteSessionData=None): + if incompleteSessionData: + self.session = MediaDetailsIncomplete(originalMedia, mediaChoice, + incompleteSessionData=incompleteSessionData) + else: + self.session = MediaDetails(sessionMedia, mediaChoice) + self.original = MediaDetails(originalMedia, mediaChoice) + + +ATTRIBUTE_TYPES = OrderedDict() + + +def registerAttributeType(cls): + ATTRIBUTE_TYPES[cls.name] = cls + return cls + + +def normRes(res): + try: + int(res) + except: + pass + else: + res += "p" + return res + + +class DPAttribute: + """ + An attribute reference to source.attr + """ + def __init__(self, attr, source="details.original"): + self.attr = attr + self.attrWithPath = "%s.%s" % (source, attr) if source else attr + + def __call__(self, *args, **kwargs): + return self.value(*args, **kwargs) + + def resolve(self, instance_or_value, obj): + """ + Resolve attribute to value based on the given type. + Returns value or Attribute.value() + + :param instance_or_value: Attribute instance or value + :param obj: VideoSessionInfo instance + :return: + """ + return instance_or_value.value(obj) if isinstance(instance_or_value, DPAttribute) else instance_or_value + + def value(self, obj): + """ + Returns value of the given path based on obj + + :param obj: VideoSessionInfo instance + :return: + """ + o = obj + for p in self.attrWithPath.split("."): + o = getattr(o, p, None) + + return o + + +class DPAttributeOriginal(DPAttribute): + pass + + +class DPAttributeSession(DPAttribute): + def __init__(self, attr): + DPAttribute.__init__(self, attr, source="details.session") + + +class DPAttributesDiffer(DPAttribute): + def __init__(self, attr, formatTrue=u"%(val1)s->%(val2)s", formatFalse=u"%(val1)s", + valueFormatter=lambda i, v1, v2: [v1, v2]): + DPAttribute.__init__(self, attr) + self.formatTrue = formatTrue + self.formatFalse = formatFalse + self.valueFormatter = valueFormatter + + def value(self, obj): + """ + Returns formatted value if values differ, otherwise the original value based on attr + + :param obj: VideoSessionInfo instance + :return: + """ + val1 = getattr(obj.details.original, self.attr, None) + val2 = getattr(obj.details.session, self.attr, None) + formatted_val1, formatted_val2 = self.valueFormatter(obj, val1, val2) + + if formatted_val2 and formatted_val1 != formatted_val2: + return self.formatTrue % {"val1": formatted_val1, "val2": formatted_val2} + if not formatted_val1: + return "" + return (self.formatFalse % {"val1": formatted_val1, "val2": formatted_val2}) if self.formatFalse else formatted_val1 + + +class DPAttributeExists(DPAttribute): + def __init__(self, attr, source="details.session", returnValue=None): + DPAttribute.__init__(self, attr, source=source) + self.returnValue = returnValue + + def value(self, obj): + """ + Returns returnValue, which may also be an Attribute instance, in case attr exists on obj + + :param obj: VideoSessionInfo instance + :return: + """ + result = DPAttribute.value(self, obj) + if self.returnValue and result: + return self.resolve(self.returnValue, obj) + + return result + + +class DPAttributeEqualsValue(DPAttribute): + def __init__(self, attr, compareTo, retVal, source="details.session"): + DPAttribute.__init__(self, attr, source=source) + self.compareTo = compareTo + self.retVal = retVal + + def value(self, obj): + """ + Returns retVal, which may also be an Attribute, if attr's value equals compareTo's value. compareTo may also + be an Attribute. + + :param obj: VideoSessionInfo instance + :return: + """ + result = DPAttribute.value(self, obj) + if result == self.resolve(self.compareTo, obj): + return self.resolve(self.retVal, obj) + + +class ComputedPPIValue: + """ + Holds the final computed attribute data for display + """ + name = None + data = None + displayCondition = None + dataPoints = [] + + @property + def label(self): + return self.name + + @property + def value(self): + return ", ".join([x for x in self.data if x not in (None, "")]) + + def __str__(self): + return "%s: %s" % (self.label, self.value) + + def __repr__(self): + return str(self) + + +@registerAttributeType +class ModePPI(ComputedPPIValue): + name = "Mode" + dataPoints = [ + DPAttributeSession("partDecision"), + DPAttributeExists("local", source="session.player", returnValue="local") + ] + + +@registerAttributeType +class ContainerPPI(ComputedPPIValue): + name = "Container" + dataPoints = [ + DPAttributesDiffer("container"), + ] + + +@registerAttributeType +class VideoPPI(ComputedPPIValue): + name = "Video" + displayCondition = DPAttributeExists("hasVideoStream", source="details.original") + dataPoints = [ + DPAttributesDiffer("videoCodec"), + DPAttributesDiffer("videoResolution", valueFormatter=lambda i, v1, v2: [normRes(v1), normRes(v2)]), + DPAttributesDiffer("videoBitrate", formatTrue=u"%(val1)s->%(val2)skbit", formatFalse=u"%(val1)skbit"), + lambda i: [ + (i.details.session.videoStreamDecision + " HW") + if i.details.session.transcodeVideoDecision == "transcode" and i.details.session.transcodeHWEncoding + else i.details.session.videoStreamDecision + ] + ] + + +@registerAttributeType +class AudioPPI(ComputedPPIValue): + name = "Audio" + displayCondition = DPAttributeExists("hasAudioStream", source="details.original") + dataPoints = [ + DPAttributesDiffer("audioCodec"), + DPAttributesDiffer("audioBitrate", formatTrue=u"%(val1)s->%(val2)skbit", formatFalse=u"%(val1)skbit"), + DPAttributesDiffer("audioChannels", formatTrue=u"%(val1)s->%(val2)sch", formatFalse=u"%(val1)sch"), + DPAttributeExists("audioStreamDecision") + ] + + +@registerAttributeType +class SubtitlesPPI(ComputedPPIValue): + name = "Subtitles" + displayCondition = DPAttributeExists("hasSubtitleStream", source="details.original") + dataPoints = [ + DPAttributesDiffer("subtitleCodec", valueFormatter=lambda i, v1, v2: [v1, + "burn" if i.details.session.subtitleBurn else v2]), + DPAttributeEqualsValue("subtitleStreamDecision", "burn", DPAttribute("subtitleStreamDecision")), + DPAttributeExists("subtitleLocation") + ] + + +@registerAttributeType +class UserPPI(ComputedPPIValue): + name = "User" + dataPoints = [ + lambda i: [u"%s @ %s" % (plexapp.ACCOUNT.title or plexapp.ACCOUNT.username or ' ', i.mediaItem.server.name)] + ] + + +class SessionAttributes(OrderedDict): + """ + Computes all the PPI instances' values + """ + def __init__(self, ref, *args, **kwargs): + self.ref = ref + OrderedDict.__init__(self, *args, **kwargs) + + for name, cls in six.iteritems(ATTRIBUTE_TYPES): + self[name] = instance = cls() + instance.data = [] + if not instance.displayCondition or instance.displayCondition(self.ref): + for dp in instance.dataPoints: + try: + # dataPoint may be a lambda or a DataPoint instance + result = dp(self.ref) + + # result may be list or value + if result is not None: + if isinstance(result, list): + instance.data += result + else: + instance.data.append(result) + except: + pass + + +class VideoSessionInfo: + def __init__(self, sessionMediaContainer, mediaContainer, incompleteSessionData=False): + self.mediaItem = mediaContainer + self.session = sessionMediaContainer + self.details = MediaDetailsHolder(self.mediaItem, self.session, mediaContainer.mediaChoice, + incompleteSessionData=incompleteSessionData) + + self.attributes = SessionAttributes(self) + diff --git a/script.plexmod/lib/backgroundthread.py b/script.plexmod/lib/backgroundthread.py new file mode 100644 index 000000000..7b69cd09d --- /dev/null +++ b/script.plexmod/lib/backgroundthread.py @@ -0,0 +1,255 @@ +from __future__ import absolute_import +import six.moves.queue +import heapq +from kodi_six import xbmc +from . import util +from plexnet import threadutils +from six.moves import range + + +class Tasks(list): + def add(self, task): + for t in self: + if not t.isValid(): + self.remove(t) + + if isinstance(task, list): + self += task + else: + self.append(task) + + def cancel(self): + while self: + self.pop().cancel() + + def kill(self): + self.cancel() + BGThreader.kill() + + +class Task: + def __init__(self, priority=None): + self._priority = priority + self._canceled = False + self.finished = False + + def __cmp__(self, other): + return self._priority - other._priority + + def __le__(self, other): + return self._priority < other._priority + + def __gt__(self, other): + return self._priority > other._priority + + def start(self): + BGThreader.addTask(self) + + def _run(self): + self.run() + self.finished = True + + def run(self): + pass + + def cancel(self): + self._canceled = True + + def isCanceled(self): + return self._canceled or util.MONITOR.abortRequested() + + def isValid(self): + return not self.finished and not self._canceled + + +class MutablePriorityQueue(six.moves.queue.PriorityQueue): + def _get(self, heappop=heapq.heappop): + self.queue.sort() + return heappop(self.queue) + + def lowest(self): + """Return the lowest priority item in the queue (not reliable!).""" + self.mutex.acquire() + try: + lowest = self.queue and min(self.queue) or None + except: + lowest = None + util.ERROR() + finally: + self.mutex.release() + return lowest + + +class BackgroundWorker: + def __init__(self, queue, name=None): + self._queue = queue + self.name = name + self._thread = None + self._abort = False + self._task = None + + def _runTask(self, task): + if task._canceled: + return + try: + task._run() + except: + util.ERROR() + + def abort(self): + self._abort = True + return self + + def aborted(self): + return self._abort or util.MONITOR.abortRequested() + + def start(self): + if self._thread and self._thread.is_alive(): + return + + self._thread = threadutils.KillableThread(target=self._queueLoop, name='BACKGROUND-WORKER({0})'.format(self.name)) + self._thread.start() + + def _queueLoop(self): + if self._queue.empty(): + return + + util.DEBUG_LOG('BGThreader: ({0}): Active'.format(self.name)) + try: + while not self.aborted(): + self._task = self._queue.get_nowait() + self._runTask(self._task) + self._queue.task_done() + self._task = None + except six.moves.queue.Empty: + util.DEBUG_LOG('BGThreader ({0}): Idle'.format(self.name)) + + def shutdown(self): + self.abort() + + if self._task: + self._task.cancel() + + if self._thread and self._thread.is_alive(): + util.DEBUG_LOG('BGThreader: thread ({0}): Waiting...'.format(self.name)) + self._thread.join() + util.DEBUG_LOG('BGThreader: thread ({0}): Done'.format(self.name)) + + def working(self): + return self._thread and self._thread.is_alive() + + def kill(self): + if self._thread and self._thread.is_alive(): + util.DEBUG_LOG('BGThreader: thread ({0}): Waiting...'.format(self.name)) + self._thread.join() + util.DEBUG_LOG('BGThreader: thread ({0}): Done'.format(self.name)) + + +class BackgroundThreader: + def __init__(self, name=None, worker_count=3): + self.name = name + self._queue = MutablePriorityQueue() + self._abort = False + self._priority = -1 + self.workers = [BackgroundWorker(self._queue, 'queue.{0}:worker.{1}'.format(self.name, x)) for x in range(worker_count)] + + def _nextPriority(self): + self._priority += 1 + return self._priority + + def abort(self): + self._abort = True + for w in self.workers: + w.abort() + return self + + def aborted(self): + return self._abort or util.MONITOR.abortRequested() + + def shutdown(self): + self.abort() + + for w in self.workers: + w.shutdown() + + def addTask(self, task): + task._priority = self._nextPriority() + self._queue.put(task) + self.startWorkers() + + def addTasks(self, tasks): + for t in tasks: + t._priority = self._nextPriority() + self._queue.put(t) + + self.startWorkers() + + def addTasksToFront(self, tasks): + lowest = self.getLowestPrority() + if lowest is None: + return self.addTasks(tasks) + + p = lowest - len(tasks) + for t in tasks: + t._priority = p + self._queue.put(t) + p += 1 + + self.startWorkers() + + def startWorkers(self): + for w in self.workers: + w.start() + + def working(self): + return not self._queue.empty() or self.hasTask() + + def hasTask(self): + return any([w.working() for w in self.workers]) + + def getLowestPrority(self): + lowest = self._queue.lowest() + if not lowest: + return None + + return lowest._priority + + def moveToFront(self, qitem): + lowest = self.getLowestPrority() + if lowest is None: + return + + qitem._priority = lowest - 1 + + def kill(self): + for w in self.workers: + w.kill() + + +class ThreaderManager: + def __init__(self): + self.index = 0 + self.abandoned = [] + self.threader = BackgroundThreader(str(self.index)) + + def __getattr__(self, name): + return getattr(self.threader, name) + + def reset(self): + if self.threader._queue.empty() and not self.threader.hasTask(): + return + + self.index += 1 + self.abandoned.append(self.threader.abort()) + self.threader = BackgroundThreader(str(self.index)) + + def shutdown(self): + self.threader.shutdown() + for a in self.abandoned: + a.shutdown() + + def kill(self): + self.threader.kill() + + +BGThreader = ThreaderManager() diff --git a/script.plexmod/lib/colors.py b/script.plexmod/lib/colors.py new file mode 100644 index 000000000..92d8d49df --- /dev/null +++ b/script.plexmod/lib/colors.py @@ -0,0 +1,79 @@ +# Constants +Background = 'FF111111' +BackgroundDark = 'FF0A0A0A' + +# OverlayVeryDark = GetAlpha(FF000000, 90), +# OverlayDark = GetAlpha(FF000000, 70), +# OverlayMed = GetAlpha(FF000000, 50), +# OverlayLht = GetAlpha(FF000000, 35), + +Border = 'FF1F1F1F' +Empty = 'FF1F1F1F' +Card = 'FF1F1F1F' +Button = 'FF1F1F1F' +ButtonDark = 'FF171717' +ButtonLht = 'FF555555' +ButtonMed = 'FF2D2D2D' +Indicator = 'FF999999' +Text = 'FFFFFFFF' +Subtitle = 'FF999999' + +# These are dependent on the regions background color +# TextLht = GetAlpha(FFFFFFFF, 90), +# TextMed = GetAlpha(FFFFFFFF, 75), +# TextDim = GetAlpha(FFFFFFFF, 50), +# TextDis = GetAlpha(FFFFFFFF, 30), +# TextDimDis = GetAlpha(FFFFFFFF, 10), + +Transparent = '00000000' +Black = 'FF000000' +Blue = 'FF0033CC' +Red = 'FFC23529' +RedAlt = 'FFD9534F' +Green = 'FF5CB85C' +Orange = 'FFCC7B19' +OrangeLight = 'FFF9BE03' + +# Component specific +# SettingsBg = 'FF2C2C2C' +# ScrollbarBg = GetAlpha(FFFFFFFF, 10), +# ScrollbarFgFocus = GetAlpha('Orange' 70), +# ScrollbarFg = GetAlpha(FFFFFFFF, 25), +# IndicatorBorder = 'FF000000' +# Separator = 'FF000000' +# Modal = GetAlpha(FF000000, 85), +# Focus = Orange, +# FocusToggle = OrangeLight, + +# ListFocusBg = GetAlpha(FF000000, 40), +# TrackActionsBg = GetAlpha(FF000000, 30), +# ListBg = GetAlpha(FFFFFFFF, 10), +# ListBgNoBlur = GetAlpha(FF666666, 40), +# ListImageBorder = 'FF595959' + +SearchBg = 'FF2D2D2D' +SearchButton = 'FF282828' +SearchButtonDark = 'FF1F1F1F' +SearchButtonLight = 'FF555555' + +InputBg = 'FF000000' +InputButton = 'FF282828' +InputButtonDark = 'FF1F1F1F' +InputButtonLight = 'FF555555' + + +class _noAlpha: + def __getattr__(self, name): + return globals()[name][2:] + +noAlpha = _noAlpha() + + +# def getAlpha(color, percent): +# if isinstance(color, basestring): +# color = COLORS[color] + +# if isinstance(color, int): +# util.ERROR_LOG(str(color) + " is not found in object") + +# return color and int((percent / 100 * 255) - 256) diff --git a/script.plexmod/lib/compat.py b/script.plexmod/lib/compat.py new file mode 100644 index 000000000..d7558a7cb --- /dev/null +++ b/script.plexmod/lib/compat.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import +import time +import datetime + +try: + datetime.datetime.strptime('0', '%H') +except TypeError: + # Fix for datetime issues with XBMC/Kodi + class new_datetime(datetime.datetime): + @classmethod + def strptime(cls, dstring, dformat): + return datetime.datetime(*(time.strptime(dstring, dformat)[0:6])) + + datetime.datetime = new_datetime + + +def timedelta_total_seconds(td): + try: + return td.total_seconds() + except: + return ((float(td.seconds) + float(td.days) * 24 * 3600) * 10**6) / 10**6 diff --git a/script.plexmod/lib/distro.py b/script.plexmod/lib/distro.py new file mode 100644 index 000000000..89e186804 --- /dev/null +++ b/script.plexmod/lib/distro.py @@ -0,0 +1,1399 @@ +#!/usr/bin/env python +# Copyright 2015,2016,2017 Nir Cohen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The ``distro`` package (``distro`` stands for Linux Distribution) provides +information about the Linux distribution it runs on, such as a reliable +machine-readable distro ID, or version information. + +It is the recommended replacement for Python's original +:py:func:`platform.linux_distribution` function, but it provides much more +functionality. An alternative implementation became necessary because Python +3.5 deprecated this function, and Python 3.8 removed it altogether. Its +predecessor function :py:func:`platform.dist` was already deprecated since +Python 2.6 and removed in Python 3.8. Still, there are many cases in which +access to OS distribution information is needed. See `Python issue 1322 +`_ for more information. +""" + +import argparse +import json +import logging +import os +import re +import shlex +import subprocess +import sys +import warnings +from typing import ( + Any, + Callable, + Dict, + Iterable, + Optional, + Sequence, + TextIO, + Tuple, + Type, +) + +try: + from typing import TypedDict +except ImportError: + # Python 3.7 + TypedDict = dict + +__version__ = "1.8.0" + + +class VersionDict(TypedDict): + major: str + minor: str + build_number: str + + +class InfoDict(TypedDict): + id: str + version: str + version_parts: VersionDict + like: str + codename: str + + +_UNIXCONFDIR = os.environ.get("UNIXCONFDIR", "/etc") +_UNIXUSRLIBDIR = os.environ.get("UNIXUSRLIBDIR", "/usr/lib") +_OS_RELEASE_BASENAME = "os-release" + +#: Translation table for normalizing the "ID" attribute defined in os-release +#: files, for use by the :func:`distro.id` method. +#: +#: * Key: Value as defined in the os-release file, translated to lower case, +#: with blanks translated to underscores. +#: +#: * Value: Normalized value. +NORMALIZED_OS_ID = { + "ol": "oracle", # Oracle Linux + "opensuse-leap": "opensuse", # Newer versions of OpenSuSE report as opensuse-leap +} + +#: Translation table for normalizing the "Distributor ID" attribute returned by +#: the lsb_release command, for use by the :func:`distro.id` method. +#: +#: * Key: Value as returned by the lsb_release command, translated to lower +#: case, with blanks translated to underscores. +#: +#: * Value: Normalized value. +NORMALIZED_LSB_ID = { + "enterpriseenterpriseas": "oracle", # Oracle Enterprise Linux 4 + "enterpriseenterpriseserver": "oracle", # Oracle Linux 5 + "redhatenterpriseworkstation": "rhel", # RHEL 6, 7 Workstation + "redhatenterpriseserver": "rhel", # RHEL 6, 7 Server + "redhatenterprisecomputenode": "rhel", # RHEL 6 ComputeNode +} + +#: Translation table for normalizing the distro ID derived from the file name +#: of distro release files, for use by the :func:`distro.id` method. +#: +#: * Key: Value as derived from the file name of a distro release file, +#: translated to lower case, with blanks translated to underscores. +#: +#: * Value: Normalized value. +NORMALIZED_DISTRO_ID = { + "redhat": "rhel", # RHEL 6.x, 7.x +} + +# Pattern for content of distro release file (reversed) +_DISTRO_RELEASE_CONTENT_REVERSED_PATTERN = re.compile( + r"(?:[^)]*\)(.*)\()? *(?:STL )?([\d.+\-a-z]*\d) *(?:esaeler *)?(.+)" +) + +# Pattern for base file name of distro release file +_DISTRO_RELEASE_BASENAME_PATTERN = re.compile(r"(\w+)[-_](release|version)$") + +# Base file names to be looked up for if _UNIXCONFDIR is not readable. +_DISTRO_RELEASE_BASENAMES = [ + "SuSE-release", + "arch-release", + "base-release", + "centos-release", + "fedora-release", + "gentoo-release", + "mageia-release", + "mandrake-release", + "mandriva-release", + "mandrivalinux-release", + "manjaro-release", + "oracle-release", + "redhat-release", + "rocky-release", + "sl-release", + "slackware-version", +] + +# Base file names to be ignored when searching for distro release file +_DISTRO_RELEASE_IGNORE_BASENAMES = ( + "debian_version", + "lsb-release", + "oem-release", + _OS_RELEASE_BASENAME, + "system-release", + "plesk-release", + "iredmail-release", +) + + +def linux_distribution(full_distribution_name: bool = True) -> Tuple[str, str, str]: + """ + .. deprecated:: 1.6.0 + + :func:`distro.linux_distribution()` is deprecated. It should only be + used as a compatibility shim with Python's + :py:func:`platform.linux_distribution()`. Please use :func:`distro.id`, + :func:`distro.version` and :func:`distro.name` instead. + + Return information about the current OS distribution as a tuple + ``(id_name, version, codename)`` with items as follows: + + * ``id_name``: If *full_distribution_name* is false, the result of + :func:`distro.id`. Otherwise, the result of :func:`distro.name`. + + * ``version``: The result of :func:`distro.version`. + + * ``codename``: The extra item (usually in parentheses) after the + os-release version number, or the result of :func:`distro.codename`. + + The interface of this function is compatible with the original + :py:func:`platform.linux_distribution` function, supporting a subset of + its parameters. + + The data it returns may not exactly be the same, because it uses more data + sources than the original function, and that may lead to different data if + the OS distribution is not consistent across multiple data sources it + provides (there are indeed such distributions ...). + + Another reason for differences is the fact that the :func:`distro.id` + method normalizes the distro ID string to a reliable machine-readable value + for a number of popular OS distributions. + """ + warnings.warn( + "distro.linux_distribution() is deprecated. It should only be used as a " + "compatibility shim with Python's platform.linux_distribution(). Please use " + "distro.id(), distro.version() and distro.name() instead.", + DeprecationWarning, + stacklevel=2, + ) + return _distro.linux_distribution(full_distribution_name) + + +def id() -> str: + """ + Return the distro ID of the current distribution, as a + machine-readable string. + + For a number of OS distributions, the returned distro ID value is + *reliable*, in the sense that it is documented and that it does not change + across releases of the distribution. + + This package maintains the following reliable distro ID values: + + ============== ========================================= + Distro ID Distribution + ============== ========================================= + "ubuntu" Ubuntu + "debian" Debian + "rhel" RedHat Enterprise Linux + "centos" CentOS + "fedora" Fedora + "sles" SUSE Linux Enterprise Server + "opensuse" openSUSE + "amzn" Amazon Linux + "arch" Arch Linux + "buildroot" Buildroot + "cloudlinux" CloudLinux OS + "exherbo" Exherbo Linux + "gentoo" GenToo Linux + "ibm_powerkvm" IBM PowerKVM + "kvmibm" KVM for IBM z Systems + "linuxmint" Linux Mint + "mageia" Mageia + "mandriva" Mandriva Linux + "parallels" Parallels + "pidora" Pidora + "raspbian" Raspbian + "oracle" Oracle Linux (and Oracle Enterprise Linux) + "scientific" Scientific Linux + "slackware" Slackware + "xenserver" XenServer + "openbsd" OpenBSD + "netbsd" NetBSD + "freebsd" FreeBSD + "midnightbsd" MidnightBSD + "rocky" Rocky Linux + "aix" AIX + "guix" Guix System + ============== ========================================= + + If you have a need to get distros for reliable IDs added into this set, + or if you find that the :func:`distro.id` function returns a different + distro ID for one of the listed distros, please create an issue in the + `distro issue tracker`_. + + **Lookup hierarchy and transformations:** + + First, the ID is obtained from the following sources, in the specified + order. The first available and non-empty value is used: + + * the value of the "ID" attribute of the os-release file, + + * the value of the "Distributor ID" attribute returned by the lsb_release + command, + + * the first part of the file name of the distro release file, + + The so determined ID value then passes the following transformations, + before it is returned by this method: + + * it is translated to lower case, + + * blanks (which should not be there anyway) are translated to underscores, + + * a normalization of the ID is performed, based upon + `normalization tables`_. The purpose of this normalization is to ensure + that the ID is as reliable as possible, even across incompatible changes + in the OS distributions. A common reason for an incompatible change is + the addition of an os-release file, or the addition of the lsb_release + command, with ID values that differ from what was previously determined + from the distro release file name. + """ + return _distro.id() + + +def name(pretty: bool = False) -> str: + """ + Return the name of the current OS distribution, as a human-readable + string. + + If *pretty* is false, the name is returned without version or codename. + (e.g. "CentOS Linux") + + If *pretty* is true, the version and codename are appended. + (e.g. "CentOS Linux 7.1.1503 (Core)") + + **Lookup hierarchy:** + + The name is obtained from the following sources, in the specified order. + The first available and non-empty value is used: + + * If *pretty* is false: + + - the value of the "NAME" attribute of the os-release file, + + - the value of the "Distributor ID" attribute returned by the lsb_release + command, + + - the value of the "" field of the distro release file. + + * If *pretty* is true: + + - the value of the "PRETTY_NAME" attribute of the os-release file, + + - the value of the "Description" attribute returned by the lsb_release + command, + + - the value of the "" field of the distro release file, appended + with the value of the pretty version ("" and "" + fields) of the distro release file, if available. + """ + return _distro.name(pretty) + + +def version(pretty: bool = False, best: bool = False) -> str: + """ + Return the version of the current OS distribution, as a human-readable + string. + + If *pretty* is false, the version is returned without codename (e.g. + "7.0"). + + If *pretty* is true, the codename in parenthesis is appended, if the + codename is non-empty (e.g. "7.0 (Maipo)"). + + Some distributions provide version numbers with different precisions in + the different sources of distribution information. Examining the different + sources in a fixed priority order does not always yield the most precise + version (e.g. for Debian 8.2, or CentOS 7.1). + + Some other distributions may not provide this kind of information. In these + cases, an empty string would be returned. This behavior can be observed + with rolling releases distributions (e.g. Arch Linux). + + The *best* parameter can be used to control the approach for the returned + version: + + If *best* is false, the first non-empty version number in priority order of + the examined sources is returned. + + If *best* is true, the most precise version number out of all examined + sources is returned. + + **Lookup hierarchy:** + + In all cases, the version number is obtained from the following sources. + If *best* is false, this order represents the priority order: + + * the value of the "VERSION_ID" attribute of the os-release file, + * the value of the "Release" attribute returned by the lsb_release + command, + * the version number parsed from the "" field of the first line + of the distro release file, + * the version number parsed from the "PRETTY_NAME" attribute of the + os-release file, if it follows the format of the distro release files. + * the version number parsed from the "Description" attribute returned by + the lsb_release command, if it follows the format of the distro release + files. + """ + return _distro.version(pretty, best) + + +def version_parts(best: bool = False) -> Tuple[str, str, str]: + """ + Return the version of the current OS distribution as a tuple + ``(major, minor, build_number)`` with items as follows: + + * ``major``: The result of :func:`distro.major_version`. + + * ``minor``: The result of :func:`distro.minor_version`. + + * ``build_number``: The result of :func:`distro.build_number`. + + For a description of the *best* parameter, see the :func:`distro.version` + method. + """ + return _distro.version_parts(best) + + +def major_version(best: bool = False) -> str: + """ + Return the major version of the current OS distribution, as a string, + if provided. + Otherwise, the empty string is returned. The major version is the first + part of the dot-separated version string. + + For a description of the *best* parameter, see the :func:`distro.version` + method. + """ + return _distro.major_version(best) + + +def minor_version(best: bool = False) -> str: + """ + Return the minor version of the current OS distribution, as a string, + if provided. + Otherwise, the empty string is returned. The minor version is the second + part of the dot-separated version string. + + For a description of the *best* parameter, see the :func:`distro.version` + method. + """ + return _distro.minor_version(best) + + +def build_number(best: bool = False) -> str: + """ + Return the build number of the current OS distribution, as a string, + if provided. + Otherwise, the empty string is returned. The build number is the third part + of the dot-separated version string. + + For a description of the *best* parameter, see the :func:`distro.version` + method. + """ + return _distro.build_number(best) + + +def like() -> str: + """ + Return a space-separated list of distro IDs of distributions that are + closely related to the current OS distribution in regards to packaging + and programming interfaces, for example distributions the current + distribution is a derivative from. + + **Lookup hierarchy:** + + This information item is only provided by the os-release file. + For details, see the description of the "ID_LIKE" attribute in the + `os-release man page + `_. + """ + return _distro.like() + + +def codename() -> str: + """ + Return the codename for the release of the current OS distribution, + as a string. + + If the distribution does not have a codename, an empty string is returned. + + Note that the returned codename is not always really a codename. For + example, openSUSE returns "x86_64". This function does not handle such + cases in any special way and just returns the string it finds, if any. + + **Lookup hierarchy:** + + * the codename within the "VERSION" attribute of the os-release file, if + provided, + + * the value of the "Codename" attribute returned by the lsb_release + command, + + * the value of the "" field of the distro release file. + """ + return _distro.codename() + + +def info(pretty: bool = False, best: bool = False) -> InfoDict: + """ + Return certain machine-readable information items about the current OS + distribution in a dictionary, as shown in the following example: + + .. sourcecode:: python + + { + 'id': 'rhel', + 'version': '7.0', + 'version_parts': { + 'major': '7', + 'minor': '0', + 'build_number': '' + }, + 'like': 'fedora', + 'codename': 'Maipo' + } + + The dictionary structure and keys are always the same, regardless of which + information items are available in the underlying data sources. The values + for the various keys are as follows: + + * ``id``: The result of :func:`distro.id`. + + * ``version``: The result of :func:`distro.version`. + + * ``version_parts -> major``: The result of :func:`distro.major_version`. + + * ``version_parts -> minor``: The result of :func:`distro.minor_version`. + + * ``version_parts -> build_number``: The result of + :func:`distro.build_number`. + + * ``like``: The result of :func:`distro.like`. + + * ``codename``: The result of :func:`distro.codename`. + + For a description of the *pretty* and *best* parameters, see the + :func:`distro.version` method. + """ + return _distro.info(pretty, best) + + +def os_release_info() -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information items + from the os-release file data source of the current OS distribution. + + See `os-release file`_ for details about these information items. + """ + return _distro.os_release_info() + + +def lsb_release_info() -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information items + from the lsb_release command data source of the current OS distribution. + + See `lsb_release command output`_ for details about these information + items. + """ + return _distro.lsb_release_info() + + +def distro_release_info() -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information items + from the distro release file data source of the current OS distribution. + + See `distro release file`_ for details about these information items. + """ + return _distro.distro_release_info() + + +def uname_info() -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information items + from the distro release file data source of the current OS distribution. + """ + return _distro.uname_info() + + +def os_release_attr(attribute: str) -> str: + """ + Return a single named information item from the os-release file data source + of the current OS distribution. + + Parameters: + + * ``attribute`` (string): Key of the information item. + + Returns: + + * (string): Value of the information item, if the item exists. + The empty string, if the item does not exist. + + See `os-release file`_ for details about these information items. + """ + return _distro.os_release_attr(attribute) + + +def lsb_release_attr(attribute: str) -> str: + """ + Return a single named information item from the lsb_release command output + data source of the current OS distribution. + + Parameters: + + * ``attribute`` (string): Key of the information item. + + Returns: + + * (string): Value of the information item, if the item exists. + The empty string, if the item does not exist. + + See `lsb_release command output`_ for details about these information + items. + """ + return _distro.lsb_release_attr(attribute) + + +def distro_release_attr(attribute: str) -> str: + """ + Return a single named information item from the distro release file + data source of the current OS distribution. + + Parameters: + + * ``attribute`` (string): Key of the information item. + + Returns: + + * (string): Value of the information item, if the item exists. + The empty string, if the item does not exist. + + See `distro release file`_ for details about these information items. + """ + return _distro.distro_release_attr(attribute) + + +def uname_attr(attribute: str) -> str: + """ + Return a single named information item from the distro release file + data source of the current OS distribution. + + Parameters: + + * ``attribute`` (string): Key of the information item. + + Returns: + + * (string): Value of the information item, if the item exists. + The empty string, if the item does not exist. + """ + return _distro.uname_attr(attribute) + + +try: + from functools import cached_property +except ImportError: + # Python < 3.8 + class cached_property: # type: ignore + """A version of @property which caches the value. On access, it calls the + underlying function and sets the value in `__dict__` so future accesses + will not re-call the property. + """ + + def __init__(self, f: Callable[[Any], Any]) -> None: + self._fname = f.__name__ + self._f = f + + def __get__(self, obj: Any, owner: Type[Any]) -> Any: + assert obj is not None, f"call {self._fname} on an instance" + ret = obj.__dict__[self._fname] = self._f(obj) + return ret + + +class LinuxDistribution: + """ + Provides information about a OS distribution. + + This package creates a private module-global instance of this class with + default initialization arguments, that is used by the + `consolidated accessor functions`_ and `single source accessor functions`_. + By using default initialization arguments, that module-global instance + returns data about the current OS distribution (i.e. the distro this + package runs on). + + Normally, it is not necessary to create additional instances of this class. + However, in situations where control is needed over the exact data sources + that are used, instances of this class can be created with a specific + distro release file, or a specific os-release file, or without invoking the + lsb_release command. + """ + + def __init__( + self, + include_lsb: Optional[bool] = None, + os_release_file: str = "", + distro_release_file: str = "", + include_uname: Optional[bool] = None, + root_dir: Optional[str] = None, + include_oslevel: Optional[bool] = None, + ) -> None: + """ + The initialization method of this class gathers information from the + available data sources, and stores that in private instance attributes. + Subsequent access to the information items uses these private instance + attributes, so that the data sources are read only once. + + Parameters: + + * ``include_lsb`` (bool): Controls whether the + `lsb_release command output`_ is included as a data source. + + If the lsb_release command is not available in the program execution + path, the data source for the lsb_release command will be empty. + + * ``os_release_file`` (string): The path name of the + `os-release file`_ that is to be used as a data source. + + An empty string (the default) will cause the default path name to + be used (see `os-release file`_ for details). + + If the specified or defaulted os-release file does not exist, the + data source for the os-release file will be empty. + + * ``distro_release_file`` (string): The path name of the + `distro release file`_ that is to be used as a data source. + + An empty string (the default) will cause a default search algorithm + to be used (see `distro release file`_ for details). + + If the specified distro release file does not exist, or if no default + distro release file can be found, the data source for the distro + release file will be empty. + + * ``include_uname`` (bool): Controls whether uname command output is + included as a data source. If the uname command is not available in + the program execution path the data source for the uname command will + be empty. + + * ``root_dir`` (string): The absolute path to the root directory to use + to find distro-related information files. Note that ``include_*`` + parameters must not be enabled in combination with ``root_dir``. + + * ``include_oslevel`` (bool): Controls whether (AIX) oslevel command + output is included as a data source. If the oslevel command is not + available in the program execution path the data source will be + empty. + + Public instance attributes: + + * ``os_release_file`` (string): The path name of the + `os-release file`_ that is actually used as a data source. The + empty string if no distro release file is used as a data source. + + * ``distro_release_file`` (string): The path name of the + `distro release file`_ that is actually used as a data source. The + empty string if no distro release file is used as a data source. + + * ``include_lsb`` (bool): The result of the ``include_lsb`` parameter. + This controls whether the lsb information will be loaded. + + * ``include_uname`` (bool): The result of the ``include_uname`` + parameter. This controls whether the uname information will + be loaded. + + * ``include_oslevel`` (bool): The result of the ``include_oslevel`` + parameter. This controls whether (AIX) oslevel information will be + loaded. + + * ``root_dir`` (string): The result of the ``root_dir`` parameter. + The absolute path to the root directory to use to find distro-related + information files. + + Raises: + + * :py:exc:`ValueError`: Initialization parameters combination is not + supported. + + * :py:exc:`OSError`: Some I/O issue with an os-release file or distro + release file. + + * :py:exc:`UnicodeError`: A data source has unexpected characters or + uses an unexpected encoding. + """ + self.root_dir = root_dir + self.etc_dir = os.path.join(root_dir, "etc") if root_dir else _UNIXCONFDIR + self.usr_lib_dir = ( + os.path.join(root_dir, "usr/lib") if root_dir else _UNIXUSRLIBDIR + ) + + if os_release_file: + self.os_release_file = os_release_file + else: + etc_dir_os_release_file = os.path.join(self.etc_dir, _OS_RELEASE_BASENAME) + usr_lib_os_release_file = os.path.join( + self.usr_lib_dir, _OS_RELEASE_BASENAME + ) + + # NOTE: The idea is to respect order **and** have it set + # at all times for API backwards compatibility. + if os.path.isfile(etc_dir_os_release_file) or not os.path.isfile( + usr_lib_os_release_file + ): + self.os_release_file = etc_dir_os_release_file + else: + self.os_release_file = usr_lib_os_release_file + + self.distro_release_file = distro_release_file or "" # updated later + + is_root_dir_defined = root_dir is not None + if is_root_dir_defined and (include_lsb or include_uname or include_oslevel): + raise ValueError( + "Including subprocess data sources from specific root_dir is disallowed" + " to prevent false information" + ) + self.include_lsb = ( + include_lsb if include_lsb is not None else not is_root_dir_defined + ) + self.include_uname = ( + include_uname if include_uname is not None else not is_root_dir_defined + ) + self.include_oslevel = ( + include_oslevel if include_oslevel is not None else not is_root_dir_defined + ) + + def __repr__(self) -> str: + """Return repr of all info""" + return ( + "LinuxDistribution(" + "os_release_file={self.os_release_file!r}, " + "distro_release_file={self.distro_release_file!r}, " + "include_lsb={self.include_lsb!r}, " + "include_uname={self.include_uname!r}, " + "include_oslevel={self.include_oslevel!r}, " + "root_dir={self.root_dir!r}, " + "_os_release_info={self._os_release_info!r}, " + "_lsb_release_info={self._lsb_release_info!r}, " + "_distro_release_info={self._distro_release_info!r}, " + "_uname_info={self._uname_info!r}, " + "_oslevel_info={self._oslevel_info!r})".format(self=self) + ) + + def linux_distribution( + self, full_distribution_name: bool = True + ) -> Tuple[str, str, str]: + """ + Return information about the OS distribution that is compatible + with Python's :func:`platform.linux_distribution`, supporting a subset + of its parameters. + + For details, see :func:`distro.linux_distribution`. + """ + return ( + self.name() if full_distribution_name else self.id(), + self.version(), + self._os_release_info.get("release_codename") or self.codename(), + ) + + def id(self) -> str: + """Return the distro ID of the OS distribution, as a string. + + For details, see :func:`distro.id`. + """ + + def normalize(distro_id: str, table: Dict[str, str]) -> str: + distro_id = distro_id.lower().replace(" ", "_") + return table.get(distro_id, distro_id) + + distro_id = self.os_release_attr("id") + if distro_id: + return normalize(distro_id, NORMALIZED_OS_ID) + + distro_id = self.lsb_release_attr("distributor_id") + if distro_id: + return normalize(distro_id, NORMALIZED_LSB_ID) + + distro_id = self.distro_release_attr("id") + if distro_id: + return normalize(distro_id, NORMALIZED_DISTRO_ID) + + distro_id = self.uname_attr("id") + if distro_id: + return normalize(distro_id, NORMALIZED_DISTRO_ID) + + return "" + + def name(self, pretty: bool = False) -> str: + """ + Return the name of the OS distribution, as a string. + + For details, see :func:`distro.name`. + """ + name = ( + self.os_release_attr("name") + or self.lsb_release_attr("distributor_id") + or self.distro_release_attr("name") + or self.uname_attr("name") + ) + if pretty: + name = self.os_release_attr("pretty_name") or self.lsb_release_attr( + "description" + ) + if not name: + name = self.distro_release_attr("name") or self.uname_attr("name") + version = self.version(pretty=True) + if version: + name = f"{name} {version}" + return name or "" + + def version(self, pretty: bool = False, best: bool = False) -> str: + """ + Return the version of the OS distribution, as a string. + + For details, see :func:`distro.version`. + """ + versions = [ + self.os_release_attr("version_id"), + self.lsb_release_attr("release"), + self.distro_release_attr("version_id"), + self._parse_distro_release_content(self.os_release_attr("pretty_name")).get( + "version_id", "" + ), + self._parse_distro_release_content( + self.lsb_release_attr("description") + ).get("version_id", ""), + self.uname_attr("release"), + ] + if self.uname_attr("id").startswith("aix"): + # On AIX platforms, prefer oslevel command output. + versions.insert(0, self.oslevel_info()) + elif self.id() == "debian" or "debian" in self.like().split(): + # On Debian-like, add debian_version file content to candidates list. + versions.append(self._debian_version) + version = "" + if best: + # This algorithm uses the last version in priority order that has + # the best precision. If the versions are not in conflict, that + # does not matter; otherwise, using the last one instead of the + # first one might be considered a surprise. + for v in versions: + if v.count(".") > version.count(".") or version == "": + version = v + else: + for v in versions: + if v != "": + version = v + break + if pretty and version and self.codename(): + version = f"{version} ({self.codename()})" + return version + + def version_parts(self, best: bool = False) -> Tuple[str, str, str]: + """ + Return the version of the OS distribution, as a tuple of version + numbers. + + For details, see :func:`distro.version_parts`. + """ + version_str = self.version(best=best) + if version_str: + version_regex = re.compile(r"(\d+)\.?(\d+)?\.?(\d+)?") + matches = version_regex.match(version_str) + if matches: + major, minor, build_number = matches.groups() + return major, minor or "", build_number or "" + return "", "", "" + + def major_version(self, best: bool = False) -> str: + """ + Return the major version number of the current distribution. + + For details, see :func:`distro.major_version`. + """ + return self.version_parts(best)[0] + + def minor_version(self, best: bool = False) -> str: + """ + Return the minor version number of the current distribution. + + For details, see :func:`distro.minor_version`. + """ + return self.version_parts(best)[1] + + def build_number(self, best: bool = False) -> str: + """ + Return the build number of the current distribution. + + For details, see :func:`distro.build_number`. + """ + return self.version_parts(best)[2] + + def like(self) -> str: + """ + Return the IDs of distributions that are like the OS distribution. + + For details, see :func:`distro.like`. + """ + return self.os_release_attr("id_like") or "" + + def codename(self) -> str: + """ + Return the codename of the OS distribution. + + For details, see :func:`distro.codename`. + """ + try: + # Handle os_release specially since distros might purposefully set + # this to empty string to have no codename + return self._os_release_info["codename"] + except KeyError: + return ( + self.lsb_release_attr("codename") + or self.distro_release_attr("codename") + or "" + ) + + def info(self, pretty: bool = False, best: bool = False) -> InfoDict: + """ + Return certain machine-readable information about the OS + distribution. + + For details, see :func:`distro.info`. + """ + return dict( + id=self.id(), + version=self.version(pretty, best), + version_parts=dict( + major=self.major_version(best), + minor=self.minor_version(best), + build_number=self.build_number(best), + ), + like=self.like(), + codename=self.codename(), + ) + + def os_release_info(self) -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information + items from the os-release file data source of the OS distribution. + + For details, see :func:`distro.os_release_info`. + """ + return self._os_release_info + + def lsb_release_info(self) -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information + items from the lsb_release command data source of the OS + distribution. + + For details, see :func:`distro.lsb_release_info`. + """ + return self._lsb_release_info + + def distro_release_info(self) -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information + items from the distro release file data source of the OS + distribution. + + For details, see :func:`distro.distro_release_info`. + """ + return self._distro_release_info + + def uname_info(self) -> Dict[str, str]: + """ + Return a dictionary containing key-value pairs for the information + items from the uname command data source of the OS distribution. + + For details, see :func:`distro.uname_info`. + """ + return self._uname_info + + def oslevel_info(self) -> str: + """ + Return AIX' oslevel command output. + """ + return self._oslevel_info + + def os_release_attr(self, attribute: str) -> str: + """ + Return a single named information item from the os-release file data + source of the OS distribution. + + For details, see :func:`distro.os_release_attr`. + """ + return self._os_release_info.get(attribute, "") + + def lsb_release_attr(self, attribute: str) -> str: + """ + Return a single named information item from the lsb_release command + output data source of the OS distribution. + + For details, see :func:`distro.lsb_release_attr`. + """ + return self._lsb_release_info.get(attribute, "") + + def distro_release_attr(self, attribute: str) -> str: + """ + Return a single named information item from the distro release file + data source of the OS distribution. + + For details, see :func:`distro.distro_release_attr`. + """ + return self._distro_release_info.get(attribute, "") + + def uname_attr(self, attribute: str) -> str: + """ + Return a single named information item from the uname command + output data source of the OS distribution. + + For details, see :func:`distro.uname_attr`. + """ + return self._uname_info.get(attribute, "") + + @cached_property + def _os_release_info(self) -> Dict[str, str]: + """ + Get the information items from the specified os-release file. + + Returns: + A dictionary containing all information items. + """ + if os.path.isfile(self.os_release_file): + with open(self.os_release_file, encoding="utf-8") as release_file: + return self._parse_os_release_content(release_file) + return {} + + @staticmethod + def _parse_os_release_content(lines: TextIO) -> Dict[str, str]: + """ + Parse the lines of an os-release file. + + Parameters: + + * lines: Iterable through the lines in the os-release file. + Each line must be a unicode string or a UTF-8 encoded byte + string. + + Returns: + A dictionary containing all information items. + """ + props = {} + lexer = shlex.shlex(lines, posix=True) + lexer.whitespace_split = True + + tokens = list(lexer) + for token in tokens: + # At this point, all shell-like parsing has been done (i.e. + # comments processed, quotes and backslash escape sequences + # processed, multi-line values assembled, trailing newlines + # stripped, etc.), so the tokens are now either: + # * variable assignments: var=value + # * commands or their arguments (not allowed in os-release) + # Ignore any tokens that are not variable assignments + if "=" in token: + k, v = token.split("=", 1) + props[k.lower()] = v + + if "version" in props: + # extract release codename (if any) from version attribute + match = re.search(r"\((\D+)\)|,\s*(\D+)", props["version"]) + if match: + release_codename = match.group(1) or match.group(2) + props["codename"] = props["release_codename"] = release_codename + + if "version_codename" in props: + # os-release added a version_codename field. Use that in + # preference to anything else Note that some distros purposefully + # do not have code names. They should be setting + # version_codename="" + props["codename"] = props["version_codename"] + elif "ubuntu_codename" in props: + # Same as above but a non-standard field name used on older Ubuntus + props["codename"] = props["ubuntu_codename"] + + return props + + @cached_property + def _lsb_release_info(self) -> Dict[str, str]: + """ + Get the information items from the lsb_release command output. + + Returns: + A dictionary containing all information items. + """ + if not self.include_lsb: + return {} + try: + cmd = ("lsb_release", "-a") + stdout = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) + # Command not found or lsb_release returned error + except (OSError, subprocess.CalledProcessError): + return {} + content = self._to_str(stdout).splitlines() + return self._parse_lsb_release_content(content) + + @staticmethod + def _parse_lsb_release_content(lines: Iterable[str]) -> Dict[str, str]: + """ + Parse the output of the lsb_release command. + + Parameters: + + * lines: Iterable through the lines of the lsb_release output. + Each line must be a unicode string or a UTF-8 encoded byte + string. + + Returns: + A dictionary containing all information items. + """ + props = {} + for line in lines: + kv = line.strip("\n").split(":", 1) + if len(kv) != 2: + # Ignore lines without colon. + continue + k, v = kv + props.update({k.replace(" ", "_").lower(): v.strip()}) + return props + + @cached_property + def _uname_info(self) -> Dict[str, str]: + if not self.include_uname: + return {} + try: + cmd = ("uname", "-rs") + stdout = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) + except OSError: + return {} + content = self._to_str(stdout).splitlines() + return self._parse_uname_content(content) + + @cached_property + def _oslevel_info(self) -> str: + if not self.include_oslevel: + return "" + try: + stdout = subprocess.check_output("oslevel", stderr=subprocess.DEVNULL) + except (OSError, subprocess.CalledProcessError): + return "" + return self._to_str(stdout).strip() + + @cached_property + def _debian_version(self) -> str: + try: + with open( + os.path.join(self.etc_dir, "debian_version"), encoding="ascii" + ) as fp: + return fp.readline().rstrip() + except FileNotFoundError: + return "" + + @staticmethod + def _parse_uname_content(lines: Sequence[str]) -> Dict[str, str]: + if not lines: + return {} + props = {} + match = re.search(r"^([^\s]+)\s+([\d\.]+)", lines[0].strip()) + if match: + name, version = match.groups() + + # This is to prevent the Linux kernel version from + # appearing as the 'best' version on otherwise + # identifiable distributions. + if name == "Linux": + return {} + props["id"] = name.lower() + props["name"] = name + props["release"] = version + return props + + @staticmethod + def _to_str(bytestring: bytes) -> str: + encoding = sys.getfilesystemencoding() + return bytestring.decode(encoding) + + @cached_property + def _distro_release_info(self) -> Dict[str, str]: + """ + Get the information items from the specified distro release file. + + Returns: + A dictionary containing all information items. + """ + if self.distro_release_file: + # If it was specified, we use it and parse what we can, even if + # its file name or content does not match the expected pattern. + distro_info = self._parse_distro_release_file(self.distro_release_file) + basename = os.path.basename(self.distro_release_file) + # The file name pattern for user-specified distro release files + # is somewhat more tolerant (compared to when searching for the + # file), because we want to use what was specified as best as + # possible. + match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename) + else: + try: + basenames = [ + basename + for basename in os.listdir(self.etc_dir) + if basename not in _DISTRO_RELEASE_IGNORE_BASENAMES + and os.path.isfile(os.path.join(self.etc_dir, basename)) + ] + # We sort for repeatability in cases where there are multiple + # distro specific files; e.g. CentOS, Oracle, Enterprise all + # containing `redhat-release` on top of their own. + basenames.sort() + except OSError: + # This may occur when /etc is not readable but we can't be + # sure about the *-release files. Check common entries of + # /etc for information. If they turn out to not be there the + # error is handled in `_parse_distro_release_file()`. + basenames = _DISTRO_RELEASE_BASENAMES + for basename in basenames: + match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename) + if match is None: + continue + filepath = os.path.join(self.etc_dir, basename) + distro_info = self._parse_distro_release_file(filepath) + # The name is always present if the pattern matches. + if "name" not in distro_info: + continue + self.distro_release_file = filepath + break + else: # the loop didn't "break": no candidate. + return {} + + if match is not None: + distro_info["id"] = match.group(1) + + # CloudLinux < 7: manually enrich info with proper id. + if "cloudlinux" in distro_info.get("name", "").lower(): + distro_info["id"] = "cloudlinux" + + return distro_info + + def _parse_distro_release_file(self, filepath: str) -> Dict[str, str]: + """ + Parse a distro release file. + + Parameters: + + * filepath: Path name of the distro release file. + + Returns: + A dictionary containing all information items. + """ + try: + with open(filepath, encoding="utf-8") as fp: + # Only parse the first line. For instance, on SLES there + # are multiple lines. We don't want them... + return self._parse_distro_release_content(fp.readline()) + except OSError: + # Ignore not being able to read a specific, seemingly version + # related file. + # See https://github.com/python-distro/distro/issues/162 + return {} + + @staticmethod + def _parse_distro_release_content(line: str) -> Dict[str, str]: + """ + Parse a line from a distro release file. + + Parameters: + * line: Line from the distro release file. Must be a unicode string + or a UTF-8 encoded byte string. + + Returns: + A dictionary containing all information items. + """ + matches = _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN.match(line.strip()[::-1]) + distro_info = {} + if matches: + # regexp ensures non-None + distro_info["name"] = matches.group(3)[::-1] + if matches.group(2): + distro_info["version_id"] = matches.group(2)[::-1] + if matches.group(1): + distro_info["codename"] = matches.group(1)[::-1] + elif line: + distro_info["name"] = line.strip() + return distro_info + + +_distro = LinuxDistribution() + + +def main() -> None: + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler(sys.stdout)) + + parser = argparse.ArgumentParser(description="OS distro info tool") + parser.add_argument( + "--json", "-j", help="Output in machine readable format", action="store_true" + ) + + parser.add_argument( + "--root-dir", + "-r", + type=str, + dest="root_dir", + help="Path to the root filesystem directory (defaults to /)", + ) + + args = parser.parse_args() + + if args.root_dir: + dist = LinuxDistribution( + include_lsb=False, + include_uname=False, + include_oslevel=False, + root_dir=args.root_dir, + ) + else: + dist = _distro + + if args.json: + logger.info(json.dumps(dist.info(), indent=4, sort_keys=True)) + else: + logger.info("Name: %s", dist.name(pretty=True)) + distribution_version = dist.version(pretty=True) + logger.info("Version: %s", distribution_version) + distribution_codename = dist.codename() + logger.info("Codename: %s", distribution_codename) + + +if __name__ == "__main__": + main() diff --git a/script.plexmod/lib/exceptions.py b/script.plexmod/lib/exceptions.py new file mode 100644 index 000000000..847eb90fc --- /dev/null +++ b/script.plexmod/lib/exceptions.py @@ -0,0 +1,4 @@ +# coding=utf-8 + +class NoDataException(Exception): + pass diff --git a/script.plexmod/lib/image.py b/script.plexmod/lib/image.py new file mode 100644 index 000000000..09d778a3f --- /dev/null +++ b/script.plexmod/lib/image.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import +import os +from lib import util + +CACHE_PATH = os.path.join(util.PROFILE, 'avatars') + +if not os.path.exists(CACHE_PATH): + os.makedirs(CACHE_PATH) + + +def getImage(url, ID): + return url, '' diff --git a/script.plexmod/lib/kodijsonrpc.py b/script.plexmod/lib/kodijsonrpc.py new file mode 100644 index 000000000..86fdec219 --- /dev/null +++ b/script.plexmod/lib/kodijsonrpc.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import +from kodi_six import xbmc +import json + + +class JSONRPCMethod: + + class Exception(Exception): + pass + + def __init__(self): + self.family = None + + def __getattr__(self, method): + def handler(**kwargs): + command = { + 'jsonrpc': '2.0', + 'id': 1, + 'method': '{0}.{1}'.format(self.family, method) + } + + if kwargs: + command['params'] = kwargs + + # xbmc.log(json.dumps(command)) + ret = json.loads(xbmc.executeJSONRPC(json.dumps(command))) + + if ret: + if 'error' in ret: + raise self.Exception(ret['error']) + else: + return ret['result'] + else: + return None + + return handler + + def __call__(self, family): + self.family = family + return self + + +class KodiJSONRPC: + def __init__(self): + self.methodHandler = JSONRPCMethod() + + def __getattr__(self, family): + return self.methodHandler(family) + + +rpc = KodiJSONRPC() + + +class BuiltInMethod: + + class Exception(Exception): + pass + + def __init__(self): + self.module = None + + def __getattr__(self, method): + def handler(*args, **kwargs): + args = [str(a).replace(',', r'\,') for a in args] + for k, v in kwargs.items(): + args.append('{0}={v}'.format(k, str(v).replace(',', r'\,'))) + + if args: + command = '{0}.{1}({2})'.format(self.module, method, ','.join(args)) + else: + command = '{0}.{1}'.format(self.module, method) + + xbmc.log(command, xbmc.LOGINFO) + + xbmc.executebuiltin(command) + + return handler + + def __call__(self, *args, **kwargs): + args = [str(a).replace(',', r'\,') for a in args] + for k, v in kwargs.items(): + args.append('{0}={v}'.format(k, str(v).replace(',', r'\,'))) + + if args: + command = '{0}({1})'.format(self.module, ','.join(args)) + else: + command = '{0}'.format(self.module) + + xbmc.log(command, xbmc.LOGINFO) + + xbmc.executebuiltin(command) + + def initModule(self, module): + self.module = module + return self + + +class KodiBuiltin: + def __init__(self): + self.methodHandler = BuiltInMethod() + + def __getattr__(self, module): + return self.methodHandler.initModule(module) + + +builtin = KodiBuiltin() diff --git a/script.plexmod/lib/main.py b/script.plexmod/lib/main.py new file mode 100644 index 000000000..bd7b5cf4e --- /dev/null +++ b/script.plexmod/lib/main.py @@ -0,0 +1,178 @@ +from __future__ import absolute_import +from kodi_six import xbmc + +if xbmc.getInfoLabel('Window(10000).Property(script.plex.running)') == "1": + xbmc.executebuiltin('NotifyAll({0},{1},{2})'.format('script.plexmod', 'RESTORE', '{}')) + raise SystemExit + +import gc +import atexit +import threading +import six + +from . import plex + +from plexnet import plexapp +from plexnet import threadutils +from .windows import background, userselect, home, windowutils +from . import player +from . import backgroundthread +from . import util + +BACKGROUND = None + + +if six.PY2: + _Timer = threading._Timer +else: + _Timer = threading.Timer + + +def waitForThreads(): + util.DEBUG_LOG('Main: Checking for any remaining threads') + while len(threading.enumerate()) > 1: + for t in threading.enumerate(): + if t != threading.currentThread(): + if t.is_alive(): + util.DEBUG_LOG('Main: Waiting on: {0}...'.format(t.name)) + if isinstance(t, _Timer): + t.cancel() + + try: + t.join() + except: + util.ERROR() + + +@atexit.register +def realExit(): + xbmc.log('Main: script.plex: REALLY FINISHED', xbmc.LOGINFO) + + +def signout(): + util.setSetting('auth.token', '') + util.DEBUG_LOG('Main: Signing out...') + plexapp.ACCOUNT.signOut() + + +def main(): + global BACKGROUND + try: + with util.Cron(0.1): + BACKGROUND = background.BackgroundWindow.create(function=_main) + + tries = 0 + while not BACKGROUND.isOpen and not util.MONITOR.waitForAbort(2) and tries < 60: + if tries == 0: + util.LOG("Couldn't start main loop, other dialog open? Retrying for 120s.") + BACKGROUND.show() + tries += 1 + + if BACKGROUND.isOpen: + util.setGlobalProperty('running', '1') + BACKGROUND.modal() + del BACKGROUND + else: + util.LOG("Couldn't start main loop, exiting.") + finally: + try: + util.setGlobalProperty('running', '') + util.setGlobalProperty('stop_running', '') + except: + pass + + +def _main(): + util.DEBUG_LOG('[ STARTED: {0} -------------------------------------------------------------------- ]'.format(util.ADDON.getAddonInfo('version'))) + util.DEBUG_LOG('USER-AGENT: {0}'.format(plex.defaultUserAgent())) + background.setSplash() + + try: + while not util.MONITOR.abortRequested() and not util.getGlobalProperty('stop_running'): + if plex.init(): + background.setSplash(False) + fromSwitch = False + while not util.MONITOR.abortRequested() and not util.getGlobalProperty('stop_running'): + if ( + not plexapp.ACCOUNT.isOffline and not + plexapp.ACCOUNT.isAuthenticated and + (len(plexapp.ACCOUNT.homeUsers) > 1 or plexapp.ACCOUNT.isProtected) + + ): + result = userselect.start() + if not result: + return + elif result == 'signout': + signout() + break + elif result == 'signin': + break + elif result == 'cancel' and fromSwitch: + util.DEBUG_LOG('Main: User selection canceled, reusing previous user') + plexapp.ACCOUNT.isAuthenticated = True + + if not fromSwitch: + util.DEBUG_LOG('Main: User selected') + + try: + selectedServer = plexapp.SERVERMANAGER.selectedServer + + if not selectedServer: + background.setBusy() + util.DEBUG_LOG('Main: Waiting for selected server...') + try: + for timeout, skip_preferred, skip_owned in ((10, False, False), (10, True, True)): + plex.CallbackEvent(plexapp.util.APP, 'change:selectedServer', timeout=timeout).wait() + + selectedServer = plexapp.SERVERMANAGER.checkSelectedServerSearch(skip_preferred=skip_preferred, skip_owned=skip_owned) + if selectedServer: + break + else: + util.DEBUG_LOG('Main: Finished waiting for selected server...') + finally: + background.setBusy(False) + + util.DEBUG_LOG('Main: STARTING WITH SERVER: {0}'.format(selectedServer)) + + windowutils.HOME = home.HomeWindow.open() + util.CRON.cancelReceiver(windowutils.HOME) + + if not windowutils.HOME.closeOption: + return + + closeOption = windowutils.HOME.closeOption + + windowutils.shutdownHome() + + if closeOption == 'signout': + signout() + break + elif closeOption == 'switch': + plexapp.ACCOUNT.isAuthenticated = False + fromSwitch = True + finally: + windowutils.shutdownHome() + BACKGROUND.activate() + gc.collect(2) + + else: + break + except: + util.ERROR() + finally: + util.DEBUG_LOG('Main: SHUTTING DOWN...') + background.setShutdown() + player.shutdown() + plexapp.util.APP.preShutdown() + util.CRON.stop() + backgroundthread.BGThreader.shutdown() + plexapp.util.APP.shutdown() + waitForThreads() + background.setBusy(False) + background.setSplash(False) + background.killMonitor() + + util.DEBUG_LOG('FINISHED') + util.shutdown() + + gc.collect(2) diff --git a/script.plexmod/lib/metadata.py b/script.plexmod/lib/metadata.py new file mode 100644 index 000000000..ffa33ff7b --- /dev/null +++ b/script.plexmod/lib/metadata.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import +from plexnet import media +from .util import T + + +EXTRA_MAP = { + media.METADATA_RELATED_TRAILER: T(32201, 'Trailer'), + media.METADATA_RELATED_DELETED_SCENE: T(32202, 'Deleted Scene'), + media.METADATA_RELATED_INTERVIEW: T(32203, 'Interview'), + media.METADATA_RELATED_MUSIC_VIDEO: T(32204, 'Music Video'), + media.METADATA_RELATED_BEHIND_THE_SCENES: T(32205, 'Behind the Scenes'), + media.METADATA_RELATED_SCENE_OR_SAMPLE: T(32206, 'Scene/Sample'), + media.METADATA_RELATED_LIVE_MUSIC_VIDEO: T(32207, 'Live Music Video'), + media.METADATA_RELATED_LYRIC_MUSIC_VIDEO: T(32208, 'Lyric Music Video'), + media.METADATA_RELATED_CONCERT: T(32209, 'Concert'), + media.METADATA_RELATED_FEATURETTE: T(32210, 'Featurette'), + media.METADATA_RELATED_SHORT: T(32211, 'Short'), + media.METADATA_RELATED_OTHER: T(32212, 'Other') +} + + +API_TRANSLATION_MAP = { + 'Unknown': T(32441), + 'Embedded': T(32442), + 'Forced': T(32443), + 'Lyrics': T(32444), + 'Mono': T(32445), + 'Stereo': T(32446), + 'None': T(32447) +} + + +def apiTranslate(string): + return API_TRANSLATION_MAP.get(string) or string diff --git a/script.plexmod/lib/playback_utils.py b/script.plexmod/lib/playback_utils.py new file mode 100644 index 000000000..57c0edb25 --- /dev/null +++ b/script.plexmod/lib/playback_utils.py @@ -0,0 +1,201 @@ +# coding=utf-8 +import json +import os +from collections import namedtuple, OrderedDict + +from kodi_six import xbmc, xbmcaddon, xbmcvfs +from plexnet import plexapp + +from lib import util + + + +ADDON = xbmcaddon.Addon() + + +ATTR_MAP = { + "b": "binge_mode", + "i": "auto_skip_intro", + "c": "auto_skip_credits", + "e": "show_intro_skip_early" +} + +# I know dicts are ordered in py3, but we want to be compatible with py2. +TRANS_MAP = OrderedDict(( + ("binge_mode", 33618), + ("auto_skip_intro", 32522), + ("auto_skip_credits", 32526), + ("show_intro_skip_early", 33505) +)) + +ATTR_MAP_REV = dict((v, k) for k, v in ATTR_MAP.items()) + + +PlaybackSettings = namedtuple("PlaybackSettings", list(v for v in ATTR_MAP.values())) + + +class PlaybackManager(object): + """ + Manages the playback settings for individual shows; falls back to the global default if no specifics set + """ + version = 1 + _data = None + _currentServerUUID = None + _currentUserID = None + + transMap = TRANS_MAP + + dataPath = os.path.join(util.translatePath(ADDON.getAddonInfo("profile")), "playback_settings.json") + + # this could be a property, but w/e + glob = None + + def __init__(self): + self.reset() + # bind settings change signals + for v in ATTR_MAP.values(): + plexapp.util.APP.on('change:{}'.format(v), lambda **kwargs: self.setGlob(**kwargs)) + + plexapp.util.APP.on('change:selectedServer', lambda **kwargs: self.setServerUUID(**kwargs)) + plexapp.util.APP.on("change:user", lambda **kwargs: self.setUserID(**kwargs)) + plexapp.util.APP.on('init', lambda **kwargs: self.setUserID(**kwargs)) + + def __call__(self, obj, key=None, value=None, kv_dict=None): + # shouldn't happen + if not self._currentServerUUID or not self._currentUserID: + util.DEBUG_LOG("APP.PlaybackManager, something's wrong: ServerUUID: %s, UserID: %s" % ( + self._currentServerUUID, self._currentUserID)) + return + + csid = self._currentServerUUID + cuid = self._currentUserID + + # set + if (key is not None and value is not None) or kv_dict is not None: + # prepare value dict + if csid not in self._data: + self._data[csid] = {} + + if cuid not in self._data[csid]: + self._data[csid][cuid] = {} + + ukv = {key: value} if not kv_dict else kv_dict + + if obj.ratingKey not in self._data[csid][cuid]: + self._data[csid][cuid][obj.ratingKey] = {} + + for k, v in ukv.items(): + # don't write globals into the storage + if v != getattr(self.glob, k): + self._data[csid][cuid][obj.ratingKey][ATTR_MAP_REV[k]] = v + else: + # new val set to global default, delete specific val + if ATTR_MAP_REV[k] in self._data[csid][cuid][obj.ratingKey]: + del self._data[csid][cuid][obj.ratingKey][ATTR_MAP_REV[k]] + + # empty specific settings? clean up + if not self._data[csid][cuid][obj.ratingKey]: + del self._data[csid][cuid][obj.ratingKey] + + self.save() + return self.glob._replace(**ukv) + + if not obj.ratingKey: + return self.glob + + # get + data = self._data.get(csid, {}).get(cuid, {}).get(obj.ratingKey, None) + if data: + return self.glob._replace(**dict((ATTR_MAP[k], v) for k, v in data.items())) + return self.glob + + def reset(self): + self._data = self.load() + if plexapp.SERVERMANAGER and plexapp.SERVERMANAGER.selectedServer: + self.setServerUUID() + + if plexapp.ACCOUNT: + self.setUserID() + self.setGlob() + + def setGlob(self, skey=None, value=None, **kwargs): + if skey is not None and value is not None: + self.glob = self.glob._replace(**{skey: value}) + else: + self.glob = PlaybackSettings(**dict((k, util.getUserSetting(k, False)) for k in ATTR_MAP.values())) + + def setServerUUID(self, server=None): + if not server and not plexapp.SERVERMANAGER.selectedServer: + return + self._currentServerUUID = (server if server is not None else plexapp.SERVERMANAGER.selectedServer).uuid + + def setUserID(self, account=None, reallyChanged=False): + if not account and not plexapp.ACCOUNT: + return + self._currentUserID = (account if account is not None and reallyChanged else plexapp.ACCOUNT).ID + self.setGlob() + + def load(self): + # new load method, v1 + if os.path.isfile(self.dataPath): + try: + f = xbmcvfs.File(self.dataPath) + obj = json.loads(f.read()) + f.close() + + version = obj["version"] + data = obj["data"] + except: + util.ERROR("Couldn't load playback_settings.json") + return {} + + if version < self.version: + migratedAny = False + for v in range(version + 1, self.version + 1): + migFunc = "migrateV{}".format(v) + if hasattr(self, migFunc): + migResult, data = getattr(self, migFunc)(data) + if migResult: + util.DEBUG_LOG("Migrated playback_settings.json to format v{}".format(v)) + migratedAny = True + if migratedAny: + self.save(data=data) + + return data + + else: + # migrate legacy data + jstring = plexapp.util.INTERFACE.getRegistry("BingeModeSettings") + if not jstring: + # fallback + jstring = plexapp.util.INTERFACE.getRegistry("AutoSkipSettings") + if not jstring: + return {} + + try: + util.DEBUG_LOG("Loading old BingeModeSettings") + obj = json.loads(jstring) + migData = {} + # migrate old BM settings into new format + for serverID, userIDs in obj.items(): + migData[serverID] = {} + for userID, ratingKeys in userIDs.items(): + migData[serverID][userID] = {} + for ratingKey, value in ratingKeys.items(): + migData[serverID][userID][ratingKey] = {"b": value} + + # plexapp.util.INTERFACE.clearRegistry("BingeModeSettings") + self.save(data=migData) + return migData + except: + util.DEBUG_LOG("Couldn't parse old BingeModeSettings") + return {} + + def save(self, data=None): + try: + f = xbmcvfs.File(self.dataPath, "w") + f.write(json.dumps({"version": self.version, "data": data or self._data})) + f.close() + except: + util.ERROR("Couldn't write playback_settings.json") + return diff --git a/script.plexmod/lib/player.py b/script.plexmod/lib/player.py new file mode 100644 index 000000000..7fed192d4 --- /dev/null +++ b/script.plexmod/lib/player.py @@ -0,0 +1,1480 @@ +from __future__ import absolute_import +import base64 +import threading +import six +import re +import os + +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import backgroundthread +from . import kodijsonrpc +from . import colors +from .windows import seekdialog +from . import util +from plexnet import plexplayer +from plexnet import plexapp +from plexnet import signalsmixin +from plexnet import util as plexnetUtil +from six.moves import range + +FIVE_MINUTES_MILLIS = 300000 + + +class BasePlayerHandler(object): + def __init__(self, player, session_id=None): + self.player = player + self.media = None + self.baseOffset = 0 + self.timelineType = None + self.lastTimelineState = None + self.ignoreTimelines = False + self.queuingNext = False + self.playQueue = None + self.sessionID = session_id + + def onAVChange(self): + pass + + def onAVStarted(self): + pass + + def onPrePlayStarted(self): + pass + + def onPlayBackStarted(self): + pass + + def onPlayBackPaused(self): + pass + + def onPlayBackResumed(self): + pass + + def onPlayBackStopped(self): + pass + + def onPlayBackEnded(self): + pass + + def onPlayBackSeek(self, stime, offset): + pass + + def onPlayBackFailed(self): + pass + + def onVideoWindowOpened(self): + pass + + def onVideoWindowClosed(self): + pass + + def onVideoOSD(self): + pass + + def onSeekOSD(self): + pass + + def onMonitorInit(self): + pass + + def tick(self): + pass + + def close(self): + pass + + def setSubtitles(self, *args, **kwargs): + pass + + def getIntroOffset(self, offset=None, setSkipped=False): + pass + + def setup(self, duration, meta, offset, bif_url, **kwargs): + pass + + @property + def trueTime(self): + return self.baseOffset + self.player.currentTime + + def getCurrentItem(self): + if self.player.playerObject: + return self.player.playerObject.item + return None + + def shouldSendTimeline(self, item): + return item.ratingKey and item.getServer() + + def currentDuration(self): + if self.player.playerObject and self.player.isPlaying(): + try: + return int(self.player.getTotalTime() * 1000) + except RuntimeError: + pass + + return 0 + + def updateNowPlaying(self, force=False, refreshQueue=False, state=None, time=None): + util.DEBUG_LOG("UpdateNowPlaying: force: {0} refreshQueue: {1} state: {2}".format(force, refreshQueue, state)) + if self.ignoreTimelines: + util.DEBUG_LOG("UpdateNowPlaying: ignoring timeline as requested") + return + + item = self.getCurrentItem() + if not item: + return + + if not self.shouldSendTimeline(item): + return + + state = state or self.player.playState + # Avoid duplicates + if state == self.lastTimelineState and not force: + return + + self.lastTimelineState = state + # self.timelineTimer.reset() + + time = time or int(self.trueTime * 1000) + + # self.trigger("progress", [m, item, time]) + + if refreshQueue and self.playQueue: + self.playQueue.refreshOnTimeline = True + + plexapp.util.APP.nowplayingmanager.updatePlaybackState( + self.timelineType, self.player.playerObject, state, time, self.playQueue, duration=self.currentDuration() + ) + + def getVolume(self): + return util.rpc.Application.GetProperties(properties=["volume"])["volume"] + + +class SeekPlayerHandler(BasePlayerHandler): + NO_SEEK = 0 + SEEK_IN_PROGRESS = 2 + SEEK_PLAYLIST = 3 + SEEK_REWIND = 4 + SEEK_POST_PLAY = 5 + + MODE_ABSOLUTE = 0 + MODE_RELATIVE = 1 + + def __init__(self, player, session_id=None): + BasePlayerHandler.__init__(self, player, session_id) + self.dialog = None + self.playlist = None + self.playQueue = None + self.timelineType = 'video' + self.ended = False + self.bifURL = '' + self.title = '' + self.title2 = '' + self.seekOnStart = 0 + self.chapters = None + self.stoppedInBingeMode = False + self.inBingeMode = False + self.prePlayWitnessed = False + self.queuingNext = False + self.reset() + + def reset(self): + self.duration = 0 + self.offset = 0 + self.baseOffset = 0 + self.seeking = self.NO_SEEK + self.seekOnStart = 0 + self.mode = self.MODE_RELATIVE + self.ended = False + self.stoppedInBingeMode = False + self.prePlayWitnessed = False + self.queuingNext = False + + def setup(self, duration, meta, offset, bif_url, title='', title2='', seeking=NO_SEEK, chapters=None): + self.ended = False + self.baseOffset = offset / 1000.0 + self.seeking = seeking + self.duration = duration + self.bifURL = bif_url + self.title = title + self.title2 = title2 + self.chapters = chapters or [] + self.playedThreshold = plexapp.util.INTERFACE.getPlayedThresholdValue() + self.ignoreTimelines = False + self.queuingNext = False + self.stoppedInBingeMode = False + self.inBingeMode = False + self.prePlayWitnessed = False + self.getDialog(setup=True) + self.dialog.setup(self.duration, meta, int(self.baseOffset * 1000), self.bifURL, self.title, self.title2, + chapters=self.chapters, keepMarkerDef=seeking == self.SEEK_IN_PROGRESS) + + def getDialog(self, setup=False): + if not self.dialog: + self.dialog = seekdialog.SeekDialog.create(show=False, handler=self) + + return self.dialog + + @property + def isTranscoded(self): + return self.mode == self.MODE_RELATIVE + + @property + def isDirectPlay(self): + return self.mode == self.MODE_ABSOLUTE + + @property + def trueTime(self): + if self.isTranscoded: + return self.baseOffset + self.player.currentTime + else: + if self.seekOnStart: + return self.player.playerObject.startOffset + (self.seekOnStart / 1000) + else: + return self.player.currentTime + self.player.playerObject.startOffset + + def shouldShowPostPlay(self): + if self.playlist and self.playlist.TYPE == 'playlist': + return False + + if self.inBingeMode and not self.stoppedInBingeMode: + return False + + if (not util.advancedSettings.postplayAlways and self.player.video.duration.asInt() <= FIVE_MINUTES_MILLIS)\ + or util.advancedSettings.postplayTimeout <= 0: + return False + + return True + + def showPostPlay(self): + if not self.shouldShowPostPlay(): + util.DEBUG_LOG("SeekHandler: Not showing post-play") + return + util.DEBUG_LOG("SeekHandler: Showing post-play") + + self.seeking = self.SEEK_POST_PLAY + self.hideOSD(delete=True) + + self.player.trigger('post.play', video=self.player.video, playlist=self.playlist, handler=self, + stoppedInBingeMode=self.stoppedInBingeMode) + + self.stoppedInBingeMode = False + + return True + + def getIntroOffset(self, offset=None, setSkipped=False): + return self.getDialog().displayMarkers(onlyReturnIntroMD=True, offset=offset, setSkipped=setSkipped) + + def next(self, on_end=False): + if self.playlist and next(self.playlist): + self.seeking = self.SEEK_PLAYLIST + + if on_end: + if self.showPostPlay(): + return True + + if not self.playlist or self.stoppedInBingeMode: + return False + + xbmc.sleep(500) + + self.player.playVideoPlaylist(self.playlist, handler=self, resume=self.player.resume) + + return True + + def prev(self): + if not self.playlist or not self.playlist.prev(): + return False + + self.seeking = self.SEEK_PLAYLIST + xbmc.sleep(500) + self.player.playVideoPlaylist(self.playlist, handler=self, resume=self.player.resume) + + return True + + def playAt(self, pos): + if not self.playlist or not self.playlist.setCurrent(pos): + return False + + self.seeking = self.SEEK_PLAYLIST + self.player.playVideoPlaylist(self.playlist, handler=self, resume=self.player.resume) + + return True + + def onSeekAborted(self): + if self.seeking: + self.seeking = self.NO_SEEK + self.player.control('play') + + def showOSD(self, from_seek=False): + self.updateOffset() + if self.dialog: + self.dialog.update(self.offset, from_seek) + self.dialog.showOSD() + + def hideOSD(self, delete=False): + util.CRON.forceTick() + if self.dialog: + self.dialog.hideOSD(closing=delete) + if delete: + d = self.dialog + self.dialog = None + d.doClose() + del d + util.garbageCollect() + + def seek(self, offset, settings_changed=False, seeking=SEEK_IN_PROGRESS): + util.DEBUG_LOG( + "SeekHandler: offset={0}, settings_changed={1}, seeking={2}, state={3}".format(offset, + settings_changed, + seeking, + self.player.playState)) + if offset is None: + return + + self.offset = offset + + if self.isDirectPlay and not settings_changed: + util.DEBUG_LOG('New absolute player offset: {0}'.format(self.offset)) + + if self.player.playerObject.offsetIsValid(offset / 1000): + if self.seekAbsolute(offset): + return + + self.updateNowPlaying(state=self.player.STATE_PAUSED) # To for update after seek + + self.seeking = self.SEEK_IN_PROGRESS + + if self.player.playState == self.player.STATE_PAUSED: + self.player.pauseAfterPlaybackStarted = True + + util.DEBUG_LOG('New player offset: {0}, state: {1}'.format(self.offset, self.player.playState)) + self.player._playVideo(offset, seeking=self.seeking, force_update=settings_changed) + + def fastforward(self): + xbmc.executebuiltin('PlayerControl(forward)') + + def rewind(self): + if self.isDirectPlay: + xbmc.executebuiltin('PlayerControl(rewind)') + else: + self.seek(max(self.trueTime - 30, 0) * 1000, seeking=self.SEEK_REWIND) + + def seekAbsolute(self, seek=None): + self.seekOnStart = seek or (self.seekOnStart if self.seekOnStart else None) + if self.seekOnStart is not None: + seekSeconds = self.seekOnStart / 1000.0 + try: + if seekSeconds >= self.player.getTotalTime(): + util.DEBUG_LOG("SeekAbsolute: Bad offset: {0}".format(seekSeconds)) + return False + except RuntimeError: # Not playing a file + util.DEBUG_LOG("SeekAbsolute: runtime error") + return False + self.updateNowPlaying(state=self.player.STATE_PAUSED) # To for update after seek + + util.DEBUG_LOG("SeekAbsolute: Seeking to {0}".format(self.seekOnStart)) + self.player.seekTime(self.seekOnStart / 1000.0) + return True + + def onAVChange(self): + util.DEBUG_LOG('SeekHandler: onAVChange') + if self.dialog: + self.dialog.onAVChange() + + def onAVStarted(self): + util.DEBUG_LOG('SeekHandler: onAVStarted') + + if self.dialog: + self.dialog.onAVStarted() + + # check if embedded subtitle was set correctly + if self.isDirectPlay and self.player.video and self.player.video.current_subtitle_is_embedded: + try: + playerID = kodijsonrpc.rpc.Player.GetActivePlayers()[0]["playerid"] + currIdx = kodijsonrpc.rpc.Player.GetProperties(playerid=playerID, properties=['currentsubtitle'])[ + 'currentsubtitle']['index'] + if currIdx != self.player.video._current_subtitle_idx: + util.LOG("Embedded Subtitle index was incorrect ({}), setting to: {}". + format(currIdx, self.player.video._current_subtitle_idx)) + self.dialog.setSubtitles() + else: + util.DEBUG_LOG("Embedded subtitle was correctly set in Kodi") + except: + util.ERROR("Exception when trying to check for embedded subtitles") + + def onPrePlayStarted(self): + util.DEBUG_LOG('SeekHandler: onPrePlayStarted, DP: {}'.format(self.isDirectPlay)) + self.prePlayWitnessed = True + if self.isDirectPlay: + self.setSubtitles(do_sleep=False) + + def onPlayBackStarted(self): + util.DEBUG_LOG('SeekHandler: onPlayBackStarted, DP: {}'.format(self.isDirectPlay)) + self.updateNowPlaying(force=True, refreshQueue=True) + + if self.dialog: + self.dialog.onPlayBackStarted() + + #if not self.prePlayWitnessed and self.isDirectPlay: + if self.isDirectPlay: + self.setSubtitles(do_sleep=False) + + def onPlayBackResumed(self): + self.updateNowPlaying() + if self.dialog: + self.dialog.onPlayBackResumed() + + util.CRON.forceTick() + # self.hideOSD() + + def onPlayBackStopped(self): + util.DEBUG_LOG('SeekHandler: onPlayBackStopped - ' + 'Seeking={0}, QueueingNext={1}, BingeMode={2}'.format(self.seeking, self.queuingNext, + self.inBingeMode)) + + if self.dialog: + self.dialog.onPlayBackStopped() + + if self.queuingNext and self.inBingeMode: + if self.next(on_end=False): + return + + if self.seeking not in (self.SEEK_IN_PROGRESS, self.SEEK_REWIND): + self.updateNowPlaying() + + # show post play if possible, if an item has been watched (90% by Plex standards) + if self.seeking != self.SEEK_PLAYLIST and self.duration: + playedFac = self.trueTime * 1000 / float(self.duration) + util.DEBUG_LOG("Player - played-threshold: {}/{}".format(playedFac, self.playedThreshold)) + if playedFac >= self.playedThreshold and self.next(on_end=True): + return + + if self.seeking not in (self.SEEK_IN_PROGRESS, self.SEEK_PLAYLIST): + self.hideOSD(delete=True) + self.sessionEnded() + + def onPlayBackEnded(self): + util.DEBUG_LOG('SeekHandler: onPlayBackEnded - Seeking={0}'.format(self.seeking)) + + if self.dialog: + self.dialog.onPlayBackEnded() + + if self.player.playerObject.hasMoreParts(): + self.updateNowPlaying(state=self.player.STATE_PAUSED) # To for update after seek + self.seeking = self.SEEK_IN_PROGRESS + self.player._playVideo(self.player.playerObject.getNextPartOffset(), seeking=self.seeking) + return + + self.updateNowPlaying() + + if self.queuingNext: + util.DEBUG_LOG('SeekHandler: onPlayBackEnded - event ignored') + return + + if self.inBingeMode: + self.stoppedInBingeMode = False + + if self.playlist and self.playlist.hasNext(): + self.queuingNext = True + if self.next(on_end=True): + return + else: + self.queuingNext = False + + if not self.ended: + if self.seeking != self.SEEK_PLAYLIST: + self.hideOSD() + + if self.seeking not in (self.SEEK_IN_PROGRESS, self.SEEK_PLAYLIST): + self.sessionEnded() + + def onPlayBackPaused(self): + self.updateNowPlaying() + if self.dialog: + self.dialog.onPlayBackPaused() + + def onPlayBackSeek(self, stime, offset): + util.DEBUG_LOG('SeekHandler: onPlayBackSeek - {0}, {1}, {2}'.format(stime, offset, self.seekOnStart)) + if self.dialog: + self.dialog.onPlayBackSeek(stime, offset) + + if self.seekOnStart: + seeked = False + if self.dialog: + seeked = self.dialog.tick(stime) + + if seeked: + util.DEBUG_LOG("OnPlayBackSeek: Seeked on start to: {0}".format(stime)) + self.seekOnStart = 0 + return + + self.updateOffset() + # self.showOSD(from_seek=True) + + def setSubtitles(self, do_sleep=True, honor_forced_subtitles_override=True): + if not self.player.video: + util.LOG("Warning: SetSubtitles: no player.video object available") + return + + subs = self.player.video.selectedSubtitleStream( + forced_subtitles_override=honor_forced_subtitles_override and util.getSetting("forced_subtitles_override", + False)) + if subs: + if do_sleep: + xbmc.sleep(100) + + path = subs.getSubtitleServerPath() + if self.isDirectPlay: + self.player.showSubtitles(False) + if path: + util.DEBUG_LOG('Setting subtitle path: {0} ({1})'.format(path, subs)) + self.player.setSubtitles(path) + self.player.showSubtitles(True) + + else: + # u_til.TEST(subs.__dict__) + # u_til.TEST(self.player.video.mediaChoice.__dict__) + util.DEBUG_LOG('Enabling embedded subtitles at: {0} ({1})'.format(subs.typeIndex, subs)) + self.player.setSubtitleStream(subs.typeIndex) + self.player.showSubtitles(True) + + else: + self.player.showSubtitles(False) + + def setAudioTrack(self): + if self.isDirectPlay: + track = self.player.video.selectedAudioStream() + if track: + # only try finding the current audio stream when the BG music isn't playing and wasn't the last + # thing played, because currentaudiostream doesn't populate for audio-only items; in that case, + # always select the proper audio stream + if not self.player.lastPlayWasBGM: + try: + playerID = kodijsonrpc.rpc.Player.GetActivePlayers()[0]["playerid"] + currIdx = kodijsonrpc.rpc.Player.GetProperties(playerid=playerID, properties=['currentaudiostream'])['currentaudiostream']['index'] + if currIdx == track.typeIndex: + util.DEBUG_LOG('Audio track is correct index: {0}'.format(track.typeIndex)) + return + except: + util.ERROR() + + self.player.lastPlayWasBGM = False + + xbmc.sleep(100) + util.DEBUG_LOG('Switching audio track - index: {0}'.format(track.typeIndex)) + self.player.setAudioStream(track.typeIndex) + + def updateOffset(self): + try: + self.offset = int(self.player.getTime() * 1000) + except RuntimeError: + pass + + def initPlayback(self): + self.seeking = self.NO_SEEK + + #self.setSubtitles() + if self.isTranscoded and self.player.getAvailableSubtitleStreams(): + util.DEBUG_LOG('Enabling first subtitle stream, as we\'re in DirectStream') + self.player.showSubtitles(True) + self.setAudioTrack() + + if self.isDirectPlay: + self.seekAbsolute() + + def onPlayBackFailed(self): + if self.ended: + return False + + if self.dialog: + self.dialog.onPlayBackFailed() + + util.DEBUG_LOG('SeekHandler: onPlayBackFailed - Seeking={0}'.format(self.seeking)) + if self.seeking not in (self.SEEK_IN_PROGRESS, self.SEEK_PLAYLIST): + self.sessionEnded() + + if self.seeking == self.SEEK_IN_PROGRESS: + return False + else: + self.seeking = self.NO_SEEK + + return True + + # def onSeekOSD(self): + # self.dialog.activate() + + def onVideoWindowOpened(self): + util.DEBUG_LOG('SeekHandler: onVideoWindowOpened - Seeking={0}'.format(self.seeking)) + self.getDialog().show() + + self.initPlayback() + + def onVideoWindowClosed(self): + self.hideOSD() + util.DEBUG_LOG('SeekHandler: onVideoWindowClosed - Seeking={0}'.format(self.seeking)) + if not self.seeking: + if self.player.isPlaying(): + self.player.stop() + if not self.playlist or not self.playlist.hasNext(): + if not self.shouldShowPostPlay(): + self.sessionEnded() + + def onVideoOSD(self): + # xbmc.executebuiltin('Dialog.Close(seekbar,true)') # Doesn't work :) + self.showOSD() + + def tick(self): + if self.seeking != self.SEEK_IN_PROGRESS: + self.updateNowPlaying(force=True) + + if self.dialog: + self.dialog.tick() + + def close(self): + self.hideOSD(delete=True) + + def sessionEnded(self): + if self.ended: + return + self.ended = True + util.DEBUG_LOG('Player: Video session ended') + self.player.trigger('session.ended', session_id=self.sessionID) + self.hideOSD(delete=True) + + __next__ = next + + +class AudioPlayerHandler(BasePlayerHandler): + def __init__(self, player): + BasePlayerHandler.__init__(self, player) + self.timelineType = 'music' + util.setGlobalProperty('track.ID', '') + self.extractTrackInfo() + + def extractTrackInfo(self): + if not self.player.isPlayingAudio(): + return + + plexID = None + for x in range(10): # Wait a sec (if necessary) for this to become available + try: + item = kodijsonrpc.rpc.Player.GetItem(playerid=0, properties=['comment'])['item'] + plexID = item['comment'] + except: + util.ERROR() + + if plexID: + break + xbmc.sleep(100) + + if not plexID: + return + + if not plexID.startswith('PLEX-'): + return + + util.DEBUG_LOG('Extracting track info from comment') + try: + data = plexID.split(':', 1)[-1] + from plexnet import plexobjects + track = plexobjects.PlexObject.deSerialize(base64.urlsafe_b64decode(data.encode('utf-8'))) + track.softReload() + self.media = track + pobj = plexplayer.PlexAudioPlayer(track) + self.player.playerObject = pobj + self.updatePlayQueueTrack(track) + util.setGlobalProperty('track.ID', track.ratingKey) # This is used in the skins to match a listitem + except: + util.ERROR() + + def setPlayQueue(self, pq): + self.playQueue = pq + pq.on('items.changed', self.playQueueCallback) + + def playQueueCallback(self, **kwargs): + plist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + # plist.clear() + try: + citem = kodijsonrpc.rpc.Player.GetItem(playerid=0, properties=['comment'])['item'] + plexID = citem['comment'].split(':', 1)[0] + except: + util.ERROR() + return + + current = plist.getposition() + size = plist.size() + + # Remove everything but the current track + for x in range(size - 1, current, -1): # First everything with a greater position + kodijsonrpc.rpc.Playlist.Remove(playlistid=xbmc.PLAYLIST_MUSIC, position=x) + for x in range(current): # Then anything with a lesser position + kodijsonrpc.rpc.Playlist.Remove(playlistid=xbmc.PLAYLIST_MUSIC, position=0) + + swap = None + for idx, track in enumerate(self.playQueue.items()): + tid = 'PLEX-{0}'.format(track.ratingKey) + if tid == plexID: + # Save the position of the current track in the pq + swap = idx + + url, li = self.player.createTrackListItem(track, index=idx + 1) + + plist.add(url, li) + + plist[0].setInfo('music', { + 'playcount': swap + 1, + }) + + # Now swap the track to the correct position. This seems to be the only way to update the kodi playlist position to the current track's new position + if swap is not None: + kodijsonrpc.rpc.Playlist.Swap(playlistid=xbmc.PLAYLIST_MUSIC, position1=0, position2=swap + 1) + kodijsonrpc.rpc.Playlist.Remove(playlistid=xbmc.PLAYLIST_MUSIC, position=0) + + self.player.trigger('playlist.changed') + + def updatePlayQueue(self, delay=False): + if not self.playQueue: + return + + self.playQueue.refresh(delay=delay) + + def updatePlayQueueTrack(self, track): + if not self.playQueue: + return + + self.playQueue.selectedId = track.playQueueItemID or None + + @property + def trueTime(self): + try: + return self.player.getTime() + except: + return self.player.currentTime + + def stampCurrentTime(self): + try: + self.player.currentTime = self.player.getTime() + except RuntimeError: # Not playing + pass + + def onMonitorInit(self): + self.extractTrackInfo() + self.updateNowPlaying(state='playing') + + def onPlayBackStarted(self): + self.player.lastPlayWasBGM = False + self.updatePlayQueue(delay=True) + self.extractTrackInfo() + self.updateNowPlaying(state='playing') + + def onPlayBackResumed(self): + self.updateNowPlaying(state='playing') + + def onPlayBackPaused(self): + self.updateNowPlaying(state='paused') + + def onPlayBackStopped(self): + self.updatePlayQueue() + self.updateNowPlaying(state='stopped') + self.finish() + + def onPlayBackEnded(self): + self.updatePlayQueue() + self.updateNowPlaying(state='stopped') + self.finish() + + def onPlayBackFailed(self): + return True + + def finish(self): + self.player.trigger('session.ended') + util.setGlobalProperty('track.ID', '') + + def tick(self): + if not self.player.isPlayingAudio() or util.MONITOR.abortRequested(): + return + + self.stampCurrentTime() + self.updateNowPlaying(force=True) + + +class BGMPlayerHandler(BasePlayerHandler): + def __init__(self, player, rating_key): + BasePlayerHandler.__init__(self, player) + self.timelineType = 'music' + self.currentlyPlaying = rating_key + util.setGlobalProperty('track.ID', '') + util.setGlobalProperty('theme_playing', '1') + + self.oldVolume = util.rpc.Application.GetProperties(properties=["volume"])["volume"] + + def onPlayBackStarted(self): + util.DEBUG_LOG("BGM: playing theme for %s" % self.currentlyPlaying) + self.player.bgmPlaying = True + + def _setVolume(self, vlm): + xbmc.executebuiltin("SetVolume({})".format(vlm)) + + def setVolume(self, volume=None, reset=False): + vlm = self.oldVolume if reset else volume + curVolume = self.getVolume() + + if curVolume != vlm: + util.DEBUG_LOG("BGM: {}setting volume to: {}".format("re-" if reset else "", vlm)) + self._setVolume(vlm) + else: + util.DEBUG_LOG("BGM: Volume already at {}".format(vlm)) + return + + waited = 0 + waitMax = 5 + while curVolume != vlm and waited < waitMax: + util.DEBUG_LOG("Waiting for volume to change from {} to {}".format(curVolume, vlm)) + xbmc.sleep(100) + waited += 1 + curVolume = self.getVolume() + + if waited == waitMax: + util.DEBUG_LOG("BGM: Timeout setting volume to {} (is: {}). Might have been externally changed in the " + "meantime".format(vlm, self.getVolume())) + + def resetVolume(self): + self.setVolume(reset=True) + + def onPlayBackStopped(self): + util.DEBUG_LOG("BGM: stopped theme for {}".format(self.currentlyPlaying)) + util.setGlobalProperty('theme_playing', '') + self.player.bgmPlaying = False + self.resetVolume() + + def onPlayBackEnded(self): + self.onPlayBackStopped() + + def onPlayBackFailed(self): + self.onPlayBackStopped() + + def close(self): + self.player.stopAndWait() + self.onPlayBackStopped() + + +class BGMPlayerTask(backgroundthread.Task): + def setup(self, source, player, *args, **kwargs): + self.source = source + self.player = player + return self + + def cancel(self): + self.player.stopAndWait() + self.player = None + backgroundthread.Task.cancel(self) + + def run(self): + if self.isCanceled(): + return + + self.player.play(self.source, windowed=True) + + +class PlexPlayer(xbmc.Player, signalsmixin.SignalsMixin): + STATE_STOPPED = "stopped" + STATE_PLAYING = "playing" + STATE_PAUSED = "paused" + STATE_BUFFERING = "buffering" + + OFFSET_RE = re.compile(r'(offset=)\d+') + + def __init__(self, *args, **kwargs): + xbmc.Player.__init__(self, *args, **kwargs) + signalsmixin.SignalsMixin.__init__(self) + self.handler = AudioPlayerHandler(self) + + def init(self): + self._closed = False + self._nextItem = None + self.started = False + self.bgmPlaying = False + self.lastPlayWasBGM = False + self.BGMTask = None + self.pauseAfterPlaybackStarted = False + self.video = None + self.hasOSD = False + self.hasSeekOSD = False + self.handler = AudioPlayerHandler(self) + self.playerObject = None + self.currentTime = 0 + self.thread = None + self.ignoreStopEvents = False + if xbmc.getCondVisibility('Player.HasMedia'): + self.started = True + self.resume = False + self.open() + + return self + + def open(self): + self._closed = False + self.monitor() + + def close(self, shutdown=False): + self._closed = True + + def reset(self): + self.video = None + self.started = False + self.bgmPlaying = False + self.playerObject = None + self.pauseAfterPlaybackStarted = False + self.ignoreStopEvents = False + #self.handler = AudioPlayerHandler(self) + self.currentTime = 0 + + def control(self, cmd): + if cmd == 'play': + self.pauseAfterPlaybackStarted = False + util.DEBUG_LOG('Player - Control: Command=Play') + if xbmc.getCondVisibility('Player.Paused | !Player.Playing'): + util.DEBUG_LOG('Player - Control: Playing') + xbmc.executebuiltin('PlayerControl(Play)') + elif cmd == 'pause': + util.DEBUG_LOG('Player - Control: Command=Pause') + if not xbmc.getCondVisibility('Player.Paused'): + util.DEBUG_LOG('Player - Control: Pausing') + xbmc.executebuiltin('PlayerControl(Play)') + + @property + def playState(self): + if xbmc.getCondVisibility('Player.Playing'): + return self.STATE_PLAYING + elif xbmc.getCondVisibility('Player.Caching'): + return self.STATE_BUFFERING + elif xbmc.getCondVisibility('Player.Paused'): + return self.STATE_PAUSED + + return self.STATE_STOPPED + + def videoIsFullscreen(self): + return xbmc.getCondVisibility('VideoPlayer.IsFullscreen') + + def currentTrack(self): + if self.handler.media and self.handler.media.type == 'track': + return self.handler.media + return None + + def playAt(self, path, ms): + """ + Plays the video specified by path. + Optionally set the start position with h,m,s,ms keyword args. + """ + seconds = ms / 1000.0 + + h = int(seconds / 3600) + m = int((seconds % 3600) / 60) + s = int(seconds % 60) + ms = int((seconds % 1) * 1000) + + kodijsonrpc.rpc.Player.Open( + item={'file': path}, + options={'resume': {'hours': h, 'minutes': m, 'seconds': s, 'milliseconds': ms}} + ) + + def play(self, *args, **kwargs): + self.started = False + xbmc.Player.play(self, *args, **kwargs) + + def playBackgroundMusic(self, source, volume, rating_key, *args, **kwargs): + if self.isPlaying(): + if not self.lastPlayWasBGM: + return + + else: + # don't re-queue the currently playing theme + if self.handler.currentlyPlaying == rating_key: + return + + # cancel any currently playing theme before starting the new one + else: + self.stopAndWait() + + if self.BGMTask and self.BGMTask.isValid(): + self.BGMTask.cancel() + + self.started = False + self.handler = BGMPlayerHandler(self, rating_key) + + # store current volume if it's different from the BGM volume + curVol = self.handler.getVolume() + if volume < curVol: + util.setSetting('last_good_volume', curVol) + + self.lastPlayWasBGM = True + + self.handler.setVolume(volume) + + self.BGMTask = BGMPlayerTask().setup(source, self, *args, **kwargs) + backgroundthread.BGThreader.addTask(self.BGMTask) + + def playVideo(self, video, resume=False, force_update=False, session_id=None, handler=None): + if self.bgmPlaying: + self.stopAndWait() + + self.handler = handler if handler and isinstance(handler, SeekPlayerHandler) \ + else SeekPlayerHandler(self, session_id) + + self.video = video + self.resume = resume + self.open() + self._playVideo(resume and video.viewOffset.asInt() or 0, force_update=force_update) + + def getOSSPathHint(self, meta): + # only hint the path one folder above for a movie, two folders above for TV + try: + head1, tail1 = os.path.split(meta.path) + head2, tail2 = os.path.split(head1) + if self.video.type == "episode": + head3, tail3 = os.path.split(head2) + cleaned_path = os.path.join(tail3, tail2, tail1) + else: + cleaned_path = os.path.join(tail2, tail1) + except: + cleaned_path = "" + return cleaned_path + + def _playVideo(self, offset=0, seeking=0, force_update=False, playerObject=None): + self.trigger('new.video', video=self.video) + self.trigger( + 'change.background', + url=self.video.defaultArt.asTranscodedImageURL(1920, 1080, opacity=60, background=colors.noAlpha.Background) + ) + try: + if not playerObject: + self.playerObject = plexplayer.PlexPlayer(self.video, offset, forceUpdate=force_update) + self.playerObject.build() + self.playerObject = self.playerObject.getServerDecision() + except plexplayer.DecisionFailure as e: + util.showNotification(e.reason, header=util.T(32448, 'Playback Failed!')) + return + except: + util.ERROR(notify=True) + return + + meta = self.playerObject.metadata + + # Kodi 19 will try to look for subtitles in the directory containing the file. '/' and `/file.mkv` both point + # to the file, and Kodi will happily try to read the whole file without recognizing it isn't a directory. + # To get around that, we omit the filename here since it is unnecessary. + url = meta.streamUrls[0].replace("file.mkv", "").replace("file.mp4", "") + + bifURL = self.playerObject.getBifUrl() + util.DEBUG_LOG('Playing URL(+{1}ms): {0}{2}'.format(plexnetUtil.cleanToken(url), offset, bifURL and ' - indexed' or '')) + + self.ignoreStopEvents = True + self.stopAndWait() # Stop before setting up the handler to prevent player events from causing havoc + self.ignoreStopEvents = False + + self.handler.setup(self.video.duration.asInt(), meta, offset, bifURL, title=self.video.grandparentTitle, + title2=self.video.title, seeking=seeking, chapters=self.video.chapters) + + # try to get an early intro offset so we can skip it if necessary + introOffset = None + if not offset: + # in case we're transcoded, instruct the marker handler to set the marker a skipped, so we don't re-skip it + # after seeking + probOff = self.handler.getIntroOffset(offset, setSkipped=meta.isTranscoded) + if probOff: + introOffset = probOff + + if meta.isTranscoded: + self.handler.mode = self.handler.MODE_RELATIVE + + if introOffset: + # cheat our way into an early intro skip by modifying the offset in the stream URL + util.DEBUG_LOG("Immediately seeking behind intro: {}".format(introOffset)) + url = self.OFFSET_RE.sub(r"\g<1>{}".format(introOffset // 1000), url) + self.handler.dialog.baseOffset = introOffset + + # probably not necessary + meta.playStart = introOffset // 1000 + else: + if offset: + util.DEBUG_LOG("Using as SeekOnStart: {0}; offset: {1}".format(meta.playStart, offset)) + self.handler.seekOnStart = meta.playStart * 1000 + elif introOffset: + util.DEBUG_LOG("Seeking behind intro after playstart: {}".format(introOffset)) + self.handler.seekOnStart = introOffset + + self.handler.mode = self.handler.MODE_ABSOLUTE + + url = util.addURLParams(url, { + 'X-Plex-Client-Profile-Name': 'Generic', + 'X-Plex-Client-Identifier': plexapp.util.INTERFACE.getGlobal('clientIdentifier') + }) + li = xbmcgui.ListItem(self.video.title, path=url) + vtype = self.video.type if self.video.type in ('movie', 'episode', 'musicvideo') else 'video' + + util.setGlobalProperty("current_path", self.getOSSPathHint(meta), base='videoinfo.{0}') + util.setGlobalProperty("current_size", str(meta.size), base='videoinfo.{0}') + li.setInfo('video', { + 'mediatype': vtype, + 'title': self.video.title, + 'originaltitle': self.video.title, + 'tvshowtitle': self.video.grandparentTitle, + 'episode': vtype == "episode" and self.video.index.asInt() or '', + 'season': vtype == "episode" and self.video.parentIndex.asInt() or '', + #'year': self.video.year.asInt(), + 'plot': self.video.summary, + 'path': meta.path, + 'size': meta.size, + }) + li.setArt({ + 'poster': self.video.defaultThumb.asTranscodedImageURL(347, 518), + 'fanart': self.video.defaultArt.asTranscodedImageURL(1920, 1080), + 'thumb': self.video.defaultThumb.asTranscodedImageURL(256, 256), + }) + + self.play(url, li) + + def playVideoPlaylist(self, playlist, resume=False, handler=None, session_id=None): + if self.bgmPlaying: + self.stopAndWait() + + if handler and isinstance(handler, SeekPlayerHandler): + self.handler = handler + else: + self.handler = SeekPlayerHandler(self, session_id) + + self.handler.playlist = playlist + if playlist.isRemote: + self.handler.playQueue = playlist + self.video = playlist.current() + self.video.softReload(includeChapters=1) + self.resume = resume + self.open() + self._playVideo(resume and self.video.viewOffset.asInt() or 0, seeking=handler and handler.SEEK_PLAYLIST or 0, force_update=True) + + # def createVideoListItem(self, video, index=0): + # url = 'plugin://script.plex/play?{0}'.format(base64.urlsafe_b64encode(video.serialize())) + # li = xbmcgui.ListItem(self.video.title, path=url, thumbnailImage=self.video.defaultThumb.asTranscodedImageURL(256, 256)) + # vtype = self.video.type if self.video.vtype in ('movie', 'episode', 'musicvideo') else 'video' + # li.setInfo('video', { + # 'mediatype': vtype, + # 'playcount': index, + # 'title': video.title, + # 'tvshowtitle': video.grandparentTitle, + # 'episode': video.index.asInt(), + # 'season': video.parentIndex.asInt(), + # 'year': video.year.asInt(), + # 'plot': video.summary + # }) + # li.setArt({ + # 'poster': self.video.defaultThumb.asTranscodedImageURL(347, 518), + # 'fanart': self.video.defaultArt.asTranscodedImageURL(1920, 1080), + # }) + + # return url, li + + def playAudio(self, track, fanart=None, **kwargs): + if self.bgmPlaying: + self.stopAndWait() + + self.handler = AudioPlayerHandler(self) + url, li = self.createTrackListItem(track, fanart) + self.stopAndWait() + self.play(url, li, **kwargs) + + def playAlbum(self, album, startpos=-1, fanart=None, **kwargs): + if self.bgmPlaying: + self.stopAndWait() + + self.handler = AudioPlayerHandler(self) + plist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + plist.clear() + index = 1 + for track in album.tracks(): + url, li = self.createTrackListItem(track, fanart, index=index) + plist.add(url, li) + index += 1 + xbmc.executebuiltin('PlayerControl(RandomOff)') + self.stopAndWait() + self.play(plist, startpos=startpos, **kwargs) + + def playAudioPlaylist(self, playlist, startpos=-1, fanart=None, **kwargs): + if self.bgmPlaying: + self.stopAndWait() + + self.handler = AudioPlayerHandler(self) + plist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + plist.clear() + index = 1 + for track in playlist.items(): + url, li = self.createTrackListItem(track, fanart, index=index) + plist.add(url, li) + index += 1 + + if playlist.isRemote: + self.handler.setPlayQueue(playlist) + else: + if playlist.startShuffled: + plist.shuffle() + xbmc.executebuiltin('PlayerControl(RandomOn)') + else: + xbmc.executebuiltin('PlayerControl(RandomOff)') + self.stopAndWait() + self.play(plist, startpos=startpos, **kwargs) + + def createTrackListItem(self, track, fanart=None, index=0): + data = base64.urlsafe_b64encode(track.serialize().encode("utf8")).decode("utf8") + url = 'plugin://script.plexmod/play?{0}'.format(data) + li = xbmcgui.ListItem(track.title, path=url) + li.setInfo('music', { + 'artist': six.text_type(track.originalTitle or track.grandparentTitle), + 'title': six.text_type(track.title), + 'album': six.text_type(track.parentTitle), + 'discnumber': track.parentIndex.asInt(), + 'tracknumber': track.get('index').asInt(), + 'duration': int(track.duration.asInt() / 1000), + 'playcount': index, + 'comment': 'PLEX-{0}:{1}'.format(track.ratingKey, data) + }) + art = fanart or track.defaultArt + li.setArt({ + 'fanart': art.asTranscodedImageURL(1920, 1080), + 'landscape': util.backgroundFromArt(art), + 'thumb': track.defaultThumb.asTranscodedImageURL(800, 800), + }) + if fanart: + li.setArt({'fanart': fanart}) + return (url, li) + + def onPrePlayStarted(self): + util.DEBUG_LOG('Player - PRE-PLAY; handler: %r' % self.handler) + self.trigger('preplay.started') + if not self.handler: + return + self.handler.onPrePlayStarted() + + def onPlayBackStarted(self): + util.DEBUG_LOG('Player - STARTED') + self.trigger('playback.started') + self.started = True + if self.pauseAfterPlaybackStarted: + self.control('pause') + self.pauseAfterPlaybackStarted = False + + if not self.handler: + return + self.handler.onPlayBackStarted() + + def onAVChange(self): + util.DEBUG_LOG('Player - AVChange') + if not self.handler: + return + self.handler.onAVChange() + + def onAVStarted(self): + util.DEBUG_LOG('Player - AVStarted: {}'.format(self.handler)) + self.trigger('av.started') + if not self.handler: + return + self.handler.onAVStarted() + + def onPlayBackPaused(self): + util.DEBUG_LOG('Player - PAUSED') + if not self.handler: + return + self.handler.onPlayBackPaused() + + def onPlayBackResumed(self): + util.DEBUG_LOG('Player - RESUMED') + if not self.handler: + return + + self.handler.onPlayBackResumed() + + def onPlayBackStopped(self): + util.DEBUG_LOG('Player - STOPPED' + (not self.started and ': FAILED' or '')) + if self.ignoreStopEvents: + return + + if not self.started: + self.onPlayBackFailed() + + if not self.handler: + return + self.handler.onPlayBackStopped() + + def onPlayBackEnded(self): + util.DEBUG_LOG('Player - ENDED' + (not self.started and ': FAILED' or '')) + if self.ignoreStopEvents: + return + + if not self.started: + self.onPlayBackFailed() + + if not self.handler: + return + self.handler.onPlayBackEnded() + + def onPlayBackSeek(self, time, offset): + util.DEBUG_LOG('Player - SEEK: %i' % offset) + if not self.handler: + return + self.handler.onPlayBackSeek(time, offset) + + def onPlayBackFailed(self): + util.DEBUG_LOG('Player - FAILED: {}'.format(self.handler)) + if not self.handler: + return + + if self.handler.onPlayBackFailed(): + util.showNotification(util.T(32448, 'Playback Failed!')) + self.stopAndWait() + self.close() + # xbmcgui.Dialog().ok('Failed', 'Playback failed') + + def onVideoWindowOpened(self): + util.DEBUG_LOG('Player: Video window opened') + try: + self.handler.onVideoWindowOpened() + except: + util.ERROR() + + def onVideoWindowClosed(self): + util.DEBUG_LOG('Player: Video window closed') + try: + self.handler.onVideoWindowClosed() + # self.stop() + except: + util.ERROR() + + def onVideoOSD(self): + util.DEBUG_LOG('Player: Video OSD opened') + try: + self.handler.onVideoOSD() + except: + util.ERROR() + + def onSeekOSD(self): + util.DEBUG_LOG('Player: Seek OSD opened') + try: + self.handler.onSeekOSD() + except: + util.ERROR() + + def stopAndWait(self): + if self.isPlaying(): + util.DEBUG_LOG('Player: Stopping and waiting...') + self.stop() + while not util.MONITOR.waitForAbort(0.1) and self.isPlaying(): + pass + util.MONITOR.waitForAbort(0.2) + util.DEBUG_LOG('Player: Stopping and waiting...Done') + + def monitor(self): + if not self.thread or not self.thread.is_alive(): + self.thread = threading.Thread(target=self._monitor, name='PLAYER:MONITOR') + self.thread.start() + + def _monitor(self): + try: + while not util.MONITOR.abortRequested() and not self._closed: + if not self.isPlaying(): + util.DEBUG_LOG('Player: Idling...') + + while not self.isPlaying() and not util.MONITOR.abortRequested() and not self._closed: + util.MONITOR.waitForAbort(0.1) + + if self.isPlayingVideo(): + util.DEBUG_LOG('Monitoring video...') + self._videoMonitor() + elif self.isPlayingAudio(): + util.DEBUG_LOG('Monitoring audio...') + self._audioMonitor() + elif self.isPlaying(): + util.DEBUG_LOG('Monitoring pre-play...') + + # note: this might never be triggered depending on how fast the video playback starts. + # don't rely on it in any way. + self._preplayMonitor() + + self.handler.close() + self.close() + util.DEBUG_LOG('Player: Closed') + finally: + self.trigger('session.ended') + + def _preplayMonitor(self): + self.onPrePlayStarted() + while self.isPlaying() and not self.isPlayingVideo() and not self.isPlayingAudio() and not util.MONITOR.abortRequested() and not self._closed: + util.MONITOR.waitForAbort(0.1) + + if not self.isPlayingVideo() and not self.isPlayingAudio(): + self.onPlayBackFailed() + + def _videoMonitor(self): + hasFullScreened = False + + ct = 0 + while self.isPlayingVideo() and not util.MONITOR.abortRequested() and not self._closed: + try: + self.currentTime = self.getTime() + except RuntimeError: + break + + util.MONITOR.waitForAbort(0.1) + if xbmc.getCondVisibility('Window.IsActive(videoosd)'): + if not self.hasOSD: + self.hasOSD = True + self.onVideoOSD() + else: + self.hasOSD = False + + if xbmc.getCondVisibility('Window.IsActive(seekbar)'): + if not self.hasSeekOSD: + self.hasSeekOSD = True + self.onSeekOSD() + else: + self.hasSeekOSD = False + + if xbmc.getCondVisibility('VideoPlayer.IsFullscreen'): + if not hasFullScreened: + hasFullScreened = True + self.onVideoWindowOpened() + elif hasFullScreened and not xbmc.getCondVisibility('Window.IsVisible(busydialog)'): + hasFullScreened = False + self.onVideoWindowClosed() + + ct += 1 + if ct > 9: + ct = 0 + self.handler.tick() + + if hasFullScreened: + self.onVideoWindowClosed() + + def _audioMonitor(self): + self.started = True + self.handler.onMonitorInit() + ct = 0 + while self.isPlayingAudio() and not util.MONITOR.abortRequested() and not self._closed: + try: + self.currentTime = self.getTime() + except RuntimeError: + break + + util.MONITOR.waitForAbort(0.1) + + ct += 1 + if ct > 9: + ct = 0 + self.handler.tick() + + +def shutdown(): + global PLAYER + PLAYER.close(shutdown=True) + del PLAYER + + +PLAYER = PlexPlayer().init() diff --git a/script.plexmod/lib/plex.py b/script.plexmod/lib/plex.py new file mode 100644 index 000000000..0576f537b --- /dev/null +++ b/script.plexmod/lib/plex.py @@ -0,0 +1,454 @@ +from __future__ import absolute_import +import sys +import platform +import traceback +import uuid +import json +import threading +import time +import requests +import six + +from kodi_six import xbmc, xbmcaddon + +from plexnet import plexapp, myplex, util as plexnet_util, asyncadapter, http as pnhttp + +from .playback_utils import PlaybackManager +from . windows.settings import PlayedThresholdSetting +from . import util +from six.moves import range + +if six.PY2: + _Event = threading._Event +else: + _Event = threading.Event + + +class PlexTimer(plexapp.util.Timer): + def shouldAbort(self): + return util.MONITOR.abortRequested() + + +def abortFlag(): + return util.MONITOR.abortRequested() + + +plexapp.util.setTimer(PlexTimer) +plexapp.setAbortFlagFunction(abortFlag) + +maxVideoRes = plexapp.Res((3840, 2160)) # INTERFACE.globals["supports4k"] and plexapp.Res((3840, 2160)) or plexapp.Res((1920, 1080)) + +CLIENT_ID = util.getSetting('client.ID') +if not CLIENT_ID: + CLIENT_ID = str(uuid.uuid4()) + util.setSetting('client.ID', CLIENT_ID) + + +def defaultUserAgent(): + """Return a string representing the default user agent.""" + _implementation = platform.python_implementation() + + if _implementation == 'CPython': + _implementation_version = platform.python_version() + elif _implementation == 'PyPy': + _implementation_version = '%s.%s.%s' % (sys.pypy_version_info.major, + sys.pypy_version_info.minor, + sys.pypy_version_info.micro) + if sys.pypy_version_info.releaselevel != 'final': + _implementation_version = ''.join([_implementation_version, sys.pypy_version_info.releaselevel]) + elif _implementation == 'Jython': + _implementation_version = platform.python_version() # Complete Guess + elif _implementation == 'IronPython': + _implementation_version = platform.python_version() # Complete Guess + else: + _implementation_version = 'Unknown' + + try: + p_system = platform.system() + p_release = platform.release() + except IOError: + p_system = 'Unknown' + p_release = 'Unknown' + + return " ".join(['%s/%s' % ('PM4K', util.ADDON.getAddonInfo('version')), + '%s/%s' % ('Kodi', xbmc.getInfoLabel('System.BuildVersion').replace(' ', '-')), + '%s/%s' % (_implementation, _implementation_version), + '%s/%s' % (p_system, p_release)]) + + +class PlexInterface(plexapp.AppInterface): + _regs = { + None: {}, + } + _globals = { + 'platform': 'Kodi', + 'appVersionStr': util.ADDON.getAddonInfo('version'), + 'clientIdentifier': CLIENT_ID, + 'platformVersion': xbmc.getInfoLabel('System.BuildVersion'), + 'product': 'PM4K', + 'provides': 'player', + 'device': util.getPlatform() or plexapp.PLATFORM, + 'model': 'Unknown', + 'friendlyName': util.rpc.Settings.GetSettingValue(setting='services.devicename').get('value') or 'Kodi', + 'supports1080p60': True, + 'vp9Support': True, + 'audioChannels': '2.0', + 'transcodeVideoQualities': [ + "10", "20", "30", "30", "40", "60", "60", "75", "100", "60", "75", "90", "100", "100" + ], + 'transcodeVideoResolutions': [ + plexapp.Res((220, 180)), + plexapp.Res((220, 128)), + plexapp.Res((284, 160)), + plexapp.Res((420, 240)), + plexapp.Res((576, 320)), + plexapp.Res((720, 480)), + plexapp.Res((1024, 768)), + plexapp.Res((1280, 720)), + plexapp.Res((1280, 720)), + maxVideoRes, maxVideoRes, maxVideoRes, maxVideoRes, maxVideoRes + ], + 'transcodeVideoBitrates': [ + "64", "96", "208", "320", "720", "1500", "2000", "3000", "4000", "8000", "10000", "12000", "20000", "400000" + ], + 'deviceInfo': plexapp.DeviceInfo() + } + + bingeModeManager = None + + def getPreference(self, pref, default=None): + if pref == 'manual_connections': + return self.getManualConnections() + else: + return util.getSetting(pref, default) + + def getManualConnections(self): + conns = [] + for i in range(2): + ip = util.getSetting('manual_ip_{0}'.format(i)) + if not ip: + continue + port = util.getSetting('manual_port_{0}'.format(i), 32400) + conns.append({'connection': ip, 'port': port}) + return json.dumps(conns) + + def setPreference(self, pref, value): + util.setSetting(pref, value) + + def getRegistry(self, reg, default=None, sec=None): + if sec == 'myplex' and reg == 'MyPlexAccount': + ret = util.getSetting('{0}.{1}'.format(sec, reg), default) + if ret: + return ret + return json.dumps({'authToken': util.getSetting('auth.token')}) + else: + return util.getSetting('{0}.{1}'.format(sec, reg), default) + + def setRegistry(self, reg, value, sec=None): + util.setSetting('{0}.{1}'.format(sec, reg), value) + + def clearRegistry(self, reg, sec=None): + util.setSetting('{0}.{1}'.format(sec, reg), '') + + def addInitializer(self, sec): + pass + + def clearInitializer(self, sec): + pass + + def getGlobal(self, glbl, default=None): + if glbl == 'transcodeVideoResolutions': + maxres = self.getPreference('allow_4k', True) and plexapp.Res((3840, 2160)) or plexapp.Res((1920, 1080)) + self._globals['transcodeVideoResolutions'][-5:] = [maxres] * 5 + elif glbl == 'audioChannels': + try: + self._globals['audioChannels'] = \ + util.CHANNELMAPPING[util.rpc.Settings.GetSettingValue(setting='audiooutput.channels').get('value')] + except: + util.DEBUG_LOG("Limiting audio channel definition to 2.0 due to error: %s" % traceback.format_exc()) + self._globals['audioChannels'] = "2.0" + + return self._globals.get(glbl, default) + + def getCapabilities(self): + return '' + + def LOG(self, msg): + util.DEBUG_LOG('API: {0}'.format(msg)) + + def DEBUG_LOG(self, msg): + self.LOG('DEBUG: {0}'.format(msg)) + + def WARN_LOG(self, msg): + self.LOG('WARNING: {0}'.format(msg)) + + def ERROR_LOG(self, msg): + self.LOG('ERROR: {0}'.format(msg)) + + def ERROR(self, msg=None, err=None): + if err: + self.LOG('ERROR: {0} - {1}'.format(msg, getattr(err, "message", "Unknown Error"))) + else: + util.ERROR() + + def supportsAudioStream(self, codec, channels): + return True + # if codec = invalid then return true + + # canDownmix = (m.globals["audioDownmix"][codec] <> invalid) + # supportsSurroundSound = m.SupportsSurroundSound() + + # if not supportsSurroundSound and canDownmix then + # maxChannels = m.globals["audioDownmix"][codec] + # else + # maxChannels = firstOf(m.globals["audioDecoders"][codec], 0) + # end if + + # if maxChannels > 2 and not canDownmix and not supportsSurroundSound then + # ' It's a surround sound codec and we can't do surround sound + # supported = false + # else if maxChannels = 0 or maxChannels < channels then + # ' The codec is either unsupported or can't handle the requested channels + # supported = false + # else + # supported = true + + # return supported + + def supportsSurroundSound(self): + return True + + def getQualityIndex(self, qualityType): + if qualityType == self.QUALITY_LOCAL: + return self.getPreference("local_quality", 13) + elif qualityType == self.QUALITY_ONLINE: + return self.getPreference("online_quality", 13) + else: + return self.getPreference("remote_quality", 13) + + def getMaxResolution(self, quality_type, allow4k=False): + qualityIndex = self.getQualityIndex(quality_type) + + if qualityIndex >= 9: + if self.getPreference('allow_4k', True): + return allow4k and 2160 or 1088 + else: + return 1088 + elif qualityIndex >= 6: + return 720 + elif qualityIndex >= 5: + return 480 + else: + return 360 + + def getThemeMusicValue(self): + index = 10 - self.getPreference("theme_music", 5) + if index > 0: + return index * 10 + return 0 + + def getPlayedThresholdValue(self): + values = list(reversed(PlayedThresholdSetting.options)) + return int(values[self.getPreference("played_threshold", 1)].replace(" %", "")) / 100.0 + + +def onSmartDiscoverLocalChange(value=None, **kwargs): + plexnet_util.CHECK_LOCAL = value + plexapp.refreshResources(True) + + +def onPreferLANChange(value=None, **kwargs): + plexnet_util.LOCAL_OVER_SECURE = value + plexapp.refreshResources(True) + + +def onPreferLocalChange(**kwargs): + plexapp.refreshResources(True) + + +def onManualIPChange(**kwargs): + plexapp.refreshResources(True) + + +plexapp.util.setInterface(PlexInterface()) +plexapp.util.INTERFACE.playbackManager = PlaybackManager() +plexapp.util.APP.on('change:smart_discover_local', onSmartDiscoverLocalChange) +plexapp.util.APP.on('change:prefer_local', onPreferLANChange) +plexapp.util.APP.on('change:same_network', onPreferLocalChange) +plexapp.util.APP.on('change:manual_ip_0', onManualIPChange) +plexapp.util.APP.on('change:manual_ip_1', onManualIPChange) +plexapp.util.APP.on('change:manual_port_0', onManualIPChange) +plexapp.util.APP.on('change:manual_port_1', onManualIPChange) + +plexapp.util.CHECK_LOCAL = util.getSetting('smart_discover_local', True) +plexapp.util.LOCAL_OVER_SECURE = util.getSetting('prefer_local', False) + +# set requests timeout +TIMEOUT = float(util.advancedSettings.requestsTimeout) +CONNCHECK_TIMEOUT = float(util.advancedSettings.connCheckTimeout) +plexapp.util.TIMEOUT = TIMEOUT +plexapp.util.CONN_CHECK_TIMEOUT = asyncadapter.AsyncTimeout(CONNCHECK_TIMEOUT).setConnectTimeout(CONNCHECK_TIMEOUT) +plexapp.util.LAN_REACHABILITY_TIMEOUT = util.advancedSettings.localReachTimeout / 1000.0 +pnhttp.DEFAULT_TIMEOUT = asyncadapter.AsyncTimeout(TIMEOUT).setConnectTimeout(TIMEOUT) +asyncadapter.DEFAULT_TIMEOUT = pnhttp.DEFAULT_TIMEOUT +plexapp.util.ACCEPT_LANGUAGE = util.ACCEPT_LANGUAGE_CODE +plexapp.setUserAgent(defaultUserAgent()) + + +class CallbackEvent(plexapp.util.CompatEvent): + def __init__(self, context, signal, timeout=15, *args, **kwargs): + plexnet_util.Event.__init__(self, *args, **kwargs) + self.start = time.time() + self.context = context + self.signal = signal + self.timeout = timeout + self.context.on(self.signal, self.set) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.wait() + + def __repr__(self): + return '<{0}:{1}>'.format(self.__class__.__name__, self.signal) + + def set(self, **kwargs): + plexnet_util.Event.set(self) + + def wait(self): + if not plexnet_util.Event.wait(self, self.timeout): + util.DEBUG_LOG('{0}: TIMED-OUT'.format(self)) + self.close() + + def triggeredOrTimedOut(self, timeout=None): + try: + if time.time() - self.start > self.timeout: + util.DEBUG_LOG('{0}: TIMED-OUT'.format(self)) + return True + + if timeout: + plexnet_util.Event.wait(self, timeout) + finally: + return self.isSet() + + def close(self): + self.set() + self.context.off(self.signal, self.set) + + +def init(): + util.DEBUG_LOG('Initializing...') + + with CallbackEvent(plexapp.util.APP, 'init'): + plexapp.init() + util.DEBUG_LOG('Waiting for account initialization...') + + util.DEBUG_LOG('Account initialized: {}'.format(plexapp.ACCOUNT.ID)) + + retry = True + + while retry: + retry = False + if not plexapp.ACCOUNT.authToken: + util.DEBUG_LOG("No auth token, authorizing") + token = authorize() + + if not token: + util.DEBUG_LOG('FAILED TO AUTHORIZE') + return False + + with CallbackEvent(plexapp.util.APP, 'account:response'): + plexapp.ACCOUNT.validateToken(token) + util.DEBUG_LOG('Waiting for account initialization...') + + # if not PLEX: + # util.messageDialog('Connection Error', u'Unable to connect to any servers') + # util.DEBUG_LOG('SIGN IN: Failed to connect to any servers') + # return False + + # util.DEBUG_LOG('SIGN IN: Connected to server: {0} - {1}'.format(PLEX.friendlyName, PLEX.baseuri)) + success = requirePlexPass() + if success == 'RETRY': + retry = True + continue + + return success + + +def requirePlexPass(): + return True + # if not plexapp.ACCOUNT.hasPlexPass(): + # from windows import signin, background + # background.setSplash(False) + # w = signin.SignInPlexPass.open() + # retry = w.retry + # del w + # util.DEBUG_LOG('PlexPass required. Signing out...') + # plexapp.ACCOUNT.signOut() + # plexapp.SERVERMANAGER.clearState() + # if retry: + # return 'RETRY' + # else: + # return False + + # return True + + +def authorize(): + from .windows import signin, background + + background.setSplash(False) + + back = signin.Background.create() + + pre = signin.PreSignInWindow.open() + try: + if not pre.doSignin: + return None + finally: + del pre + + try: + while True: + pinLoginWindow = signin.PinLoginWindow.create() + try: + pl = myplex.PinLogin() + except requests.ConnectionError: + util.ERROR() + util.messageDialog(util.T(32427, 'Failed'), util.T(32449, 'Sign-in failed. Cound not connect to plex.tv')) + return + + pinLoginWindow.setPin(pl.pin) + + try: + pl.startTokenPolling() + while not pl.finished(): + if pinLoginWindow.abort: + util.DEBUG_LOG('SIGN IN: Pin login aborted') + pl.abort() + return None + xbmc.sleep(100) + else: + if not pl.expired(): + if pl.authenticationToken: + pinLoginWindow.setLinking() + return pl.authenticationToken + else: + return None + finally: + pinLoginWindow.doClose() + del pinLoginWindow + + if pl.expired(): + util.DEBUG_LOG('SIGN IN: Pin expired') + expiredWindow = signin.ExpiredWindow.open() + try: + if not expiredWindow.refresh: + util.DEBUG_LOG('SIGN IN: Pin refresh aborted') + return None + finally: + del expiredWindow + finally: + back.doClose() + del back diff --git a/script.plexmod/lib/util.py b/script.plexmod/lib/util.py new file mode 100644 index 000000000..cd7a2d5d7 --- /dev/null +++ b/script.plexmod/lib/util.py @@ -0,0 +1,1009 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +import gc +import sys +import re +import binascii +import json +import threading +import math +import time +import datetime +import contextlib +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import six +import os +import struct +import requests + +from .kodijsonrpc import rpc +from kodi_six import xbmc +from kodi_six import xbmcgui +from kodi_six import xbmcaddon +from kodi_six import xbmcvfs + +from . import colors +from plexnet import signalsmixin, plexapp + +DEBUG = True +_SHUTDOWN = False + +ADDON = xbmcaddon.Addon() + +SETTINGS_LOCK = threading.Lock() + +_build = None +# buildversion looks like: XX.X[-TAG] (a+.b+.c+) (.+); there are kodi builds that don't set the build version +sys_ver = xbmc.getInfoLabel('System.BuildVersion') +_ver = sys_ver +if ' ' in sys_ver and '(' in sys_ver: + _ver, _build = sys_ver.split()[:2] + +_splitver = _ver.split(".") +KODI_VERSION_MAJOR, KODI_VERSION_MINOR = int(_splitver[0].split("-")[0].strip()), \ + int(_splitver[1].split("-")[0].strip()) + +_bmajor, _bminor, _bpatch = (KODI_VERSION_MAJOR, KODI_VERSION_MINOR, 0) +if _build: + _bmajor, _bminor, _bpatch = _build[1:-1].split(".") +else: + xbmc.log('script.plex: Couldn\'t determine build version, falling back to Kodi version', xbmc.LOGINFO) + +# calculate a comparable build number +KODI_BUILD_NUMBER = int("{0}{1:02d}{2:03d}".format(_bmajor, int(_bminor), int(_bpatch))) +xbmc.log('script.plex: Kodi {0}.{1} (build {2})'.format(KODI_VERSION_MAJOR, KODI_VERSION_MINOR, KODI_BUILD_NUMBER), + xbmc.LOGINFO) + + +if KODI_VERSION_MAJOR > 18: + translatePath = xbmcvfs.translatePath +else: + translatePath = xbmc.translatePath + +PROFILE = translatePath(ADDON.getAddonInfo('profile')) + + +def getChannelMapping(): + data = rpc.Settings.GetSettings(filter={"section": "system", "category": "audio"})["settings"] + return list(filter(lambda i: i["id"] == "audiooutput.channels", data))[0]["options"] + + +# retrieve labels for mapping audio channel settings values +try: + CHANNELMAPPING = dict((t["value"], t["label"]) for t in getChannelMapping()) +except: + CHANNELMAPPING = None + + +def getLanguageCode(add_def=None): + data = rpc.Settings.GetSettingValue(setting='locale.language')['value'].replace('resource.language.', '') + lang = "" + if "_" in data: + base, variant = data.split("_") + lang += "{}-{},{}".format(base, variant.upper(), base) + else: + lang = data + if add_def and lang not in add_def: + lang += ",{}".format(add_def) + return lang + + +try: + ACCEPT_LANGUAGE_CODE = getLanguageCode(add_def='en-US,en') +except: + ACCEPT_LANGUAGE_CODE = 'en-US,en' + + +def getSetting(key, default=None): + with SETTINGS_LOCK: + setting = ADDON.getSetting(key) + return _processSetting(setting, default) + + +def getUserSetting(key, default=None): + if not plexapp.ACCOUNT: + return default + + key = '{}.{}'.format(key, plexapp.ACCOUNT.ID) + with SETTINGS_LOCK: + setting = ADDON.getSetting(key) + return _processSetting(setting, default) + + +def _processSetting(setting, default): + if not setting: + return default + if isinstance(default, bool): + return setting.lower() == 'true' + elif isinstance(default, float): + return float(setting) + elif isinstance(default, int): + return int(float(setting or 0)) + elif isinstance(default, list): + if setting: + return json.loads(binascii.unhexlify(setting)) + else: + return default + + return setting + + +class AdvancedSettings(object): + """ + @DynamicAttrs + """ + + _proxiedSettings = ( + ("debug", False), + ("kodi_skip_stepping", False), + ("auto_seek", True), + ("auto_seek_delay", 1), + ("dynamic_timeline_seek", False), + ("fast_back", False), + ("dynamic_backgrounds", True), + ("background_art_blur_amount2", 0), + ("background_art_opacity_amount2", 20), + ("screensaver_quiz", False), + ("postplay_always", False), + ("postplay_timeout", 16), + ("skip_intro_button_timeout", 10), + ("skip_credits_button_timeout", 10), + ("playlist_visit_media", True), + ("intro_skip_early", False), + ("show_media_ends_info", True), + ("show_media_ends_label", True), + ("background_colour", None), + ("oldprofile", False), + ("skip_intro_button_show_early_threshold1", 60), + ("requests_timeout", 5.0), + ("local_reach_timeout", 10), + ("auto_skip_offset", 2.5), + ("conn_check_timeout", 2.5), + ("postplayCancel", True), + ("skip_marker_timer_cancel", True), + ("skip_marker_timer_immediate", False), + ("low_drift_timer", True), + ("player_show_buffer", True), + ("buffer_wait_max", 120), + ("buffer_insufficient_wait", 10), + ("continue_use_thumb", True), + ("use_bg_fallback", False), + ("dbg_crossfade", True), + ("subtitle_use_extended_title", True), + ("dialog_flicker_fix", True), + ("poster_resolution_scale_perc", 100), + ) + + def __init__(self): + # register every known setting camelCased as an attribute to this instance + for setting, default in self._proxiedSettings: + name_split = setting.split("_") + setattr(self, name_split[0] + ''.join(x.capitalize() or '_' for x in name_split[1:]), + getSetting(setting, default)) + + +advancedSettings = AdvancedSettings() + + +def LOG(msg, level=xbmc.LOGINFO): + xbmc.log('script.plex: {0}'.format(msg), level) + + +def DEBUG_LOG(msg): + if _SHUTDOWN: + return + + if not advancedSettings.debug and not xbmc.getCondVisibility('System.GetBool(debug.showloginfo)'): + return + + LOG(msg) + + +def ERROR(txt='', hide_tb=False, notify=False, time_ms=3000): + short = str(sys.exc_info()[1]) + if hide_tb: + xbmc.log('script.plex: ERROR: {0} - {1}'.format(txt, short), xbmc.LOGERROR) + return short + + import traceback + tb = traceback.format_exc() + xbmc.log("_________________________________________________________________________________", xbmc.LOGERROR) + xbmc.log('script.plex: ERROR: ' + txt, xbmc.LOGERROR) + for l in tb.splitlines(): + xbmc.log(' ' + l, xbmc.LOGERROR) + xbmc.log("_________________________________________________________________________________", xbmc.LOGERROR) + xbmc.log("`", xbmc.LOGERROR) + if notify: + showNotification('ERROR: {0}'.format(txt or short), time_ms=time_ms) + return short + + +def TEST(msg): + xbmc.log('---TEST: {0}'.format(msg), xbmc.LOGINFO) + + +class UtilityMonitor(xbmc.Monitor, signalsmixin.SignalsMixin): + def __init__(self, *args, **kwargs): + xbmc.Monitor.__init__(self, *args, **kwargs) + signalsmixin.SignalsMixin.__init__(self) + + def watchStatusChanged(self): + self.trigger('changed.watchstatus') + + def actionStop(self): + self.stopPlayback() + + def actionQuit(self): + LOG('OnSleep: Exit Kodi') + xbmc.executebuiltin('Quit') + + def actionReboot(self): + LOG('OnSleep: Reboot') + xbmc.restart() + + def actionShutdown(self): + LOG('OnSleep: Shutdown') + xbmc.shutdown() + + def actionHibernate(self): + LOG('OnSleep: Hibernate') + xbmc.executebuiltin('Hibernate') + + def actionSuspend(self): + LOG('OnSleep: Suspend') + xbmc.executebuiltin('Suspend') + + def actionCecstandby(self): + LOG('OnSleep: CEC Standby') + xbmc.executebuiltin('CECStandby') + + def actionLogoff(self): + LOG('OnSleep: Sign Out') + xbmc.executebuiltin('System.LogOff') + + def onNotification(self, sender, method, data): + LOG("Notification: {} {} {}".format(sender, method, data)) + if sender == 'script.plexmod' and method.endswith('RESTORE'): + from .windows import kodigui + if not kodigui.BaseFunctions.lastWinID: + ERROR("Addon never properly started, can't reactivate") + setGlobalProperty('stop_running', '1') + return + if kodigui.BaseFunctions.lastWinID > 13000: + xbmc.executebuiltin('ActivateWindow({0})'.format(kodigui.BaseFunctions.lastWinID)) + else: + ERROR("Addon never properly started, can't reactivate") + setGlobalProperty('stop_running', '1') + return + + getAdvancedSettings() + populateTimeFormat() + + elif sender == "xbmc" and method == "System.OnSleep" and getSetting('action_on_sleep', "none") != "none": + getattr(self, "action{}".format(getSetting('action_on_sleep', "none").capitalize()))() + + def stopPlayback(self): + LOG('Monitor: Stopping media playback') + xbmc.Player().stop() + + def onScreensaverActivated(self): + DEBUG_LOG("Monitor: OnScreensaverActivated") + if getSetting('player_stop_on_screensaver', True) and xbmc.Player().isPlayingVideo(): + self.stopPlayback() + + def onDPMSActivated(self): + DEBUG_LOG("Monitor: OnDPMSActivated") + #self.stopPlayback() + + +MONITOR = UtilityMonitor() + +ADV_MSIZE_RE = re.compile(r'(\d+)') +ADV_RFACT_RE = re.compile(r'(\d+)') +ADV_CACHE_RE = re.compile(r'\s*.*', re.S | re.I) + + +class KodiCacheManager(object): + """ + A pretty cheap approach at managing the section of advancedsettings.xml + + Starting with build 20.90.821 (Kodi 21.0-BETA2) a lot of caching issues have been fixed and + readfactor behaves better. We need to adjust for that. + """ + _cleanData = None + useModernAPI = False + memorySize = 20 # in MB + readFactor = 4 + defRF = 4 + defRFSM = 20 + recRFRange = (4, 10) + template = None + orig_tpl_path = os.path.join(ADDON.getAddonInfo('path'), "pm4k_cache_template.xml") + custom_tpl_path = "special://profile/pm4k_cache_template.xml" + translated_ctpl_path = translatePath(custom_tpl_path) + + # give Android a little more leeway with its sometimes weird memory management; otherwise stick with 23% of free mem + safeFactor = .20 if xbmc.getCondVisibility('System.Platform.Android') else .23 + + def __init__(self): + if KODI_BUILD_NUMBER >= 2090821: + self.memorySize = rpc.Settings.GetSettingValue(setting='filecache.memorysize')['value'] + self.readFactor = rpc.Settings.GetSettingValue(setting='filecache.readfactor')['value'] / 100.0 + if self.readFactor % 1 == 0: + self.readFactor = int(self.readFactor) + DEBUG_LOG("Not using advancedsettings.xml for cache/buffer management, we're at least Kodi 21 non-alpha") + self.useModernAPI = True + self.defRFSM = 7 + self.recRFRange = (1.5, 4) + + else: + self.load() + self.template = self.getTemplate() + + plexapp.util.APP.on('change:slow_connection', + lambda value=None, **kwargs: self.write(readFactor=value and self.defRFSM or self.defRF)) + + def getTemplate(self): + if xbmcvfs.exists(self.custom_tpl_path): + try: + f = xbmcvfs.File(self.custom_tpl_path) + data = f.read() + f.close() + if data: + return data + except: + pass + + DEBUG_LOG("Custom pm4k_cache_template.xml not found, using default") + f = xbmcvfs.File(self.orig_tpl_path) + data = f.read() + f.close() + return data + + def load(self): + try: + f = xbmcvfs.File("special://profile/advancedsettings.xml") + data = f.read() + f.close() + except: + LOG('script.plex: No advancedsettings.xml found') + else: + cachexml_match = ADV_CACHE_RE.search(data) + if cachexml_match: + cachexml = cachexml_match.group(0) + + try: + self.memorySize = int(ADV_MSIZE_RE.search(cachexml).group(1)) // 1024 // 1024 + except (ValueError, IndexError, TypeError): + DEBUG_LOG("script.plex: invalid or not found memorysize in advancedsettings.xml") + + try: + self.readFactor = int(ADV_RFACT_RE.search(cachexml).group(1)) + except (ValueError, IndexError, TypeError): + DEBUG_LOG("script.plex: invalid or not found readfactor in advancedsettings.xml") + + self._cleanData = data.replace(cachexml, "") + else: + self._cleanData = data + + def write(self, memorySize=None, readFactor=None): + memorySize = self.memorySize = memorySize if memorySize is not None else self.memorySize + readFactor = self.readFactor = readFactor if readFactor is not None else self.readFactor + + if self.useModernAPI: + # kodi cache settings have moved to Services>Caching + try: + rpc.Settings.SetSettingValue(setting='filecache.memorysize', value=self.memorySize) + rpc.Settings.SetSettingValue(setting='filecache.readfactor', value=int(self.readFactor * 100)) + except: + pass + return + + cd = self._cleanData + if not cd: + cd = "\n" + + finalxml = "{}\n".format( + cd.replace("", self.template.format(memorysize=memorySize * 1024 * 1024, + readfactor=readFactor)) + ) + + try: + f = xbmcvfs.File("special://profile/advancedsettings.xml", "w") + f.write(finalxml) + f.close() + except: + ERROR("Couldn't write advancedsettings.xml") + + def clamp16(self, x): + return x - x % 16 + + @property + def viableOptions(self): + default = list(filter(lambda x: x < self.recMax, + [16, 20, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024])) + + # add option to overcommit slightly + overcommit = [] + if xbmc.getCondVisibility('System.Platform.Android'): + overcommit.append(min(self.clamp16(int(self.free * 0.23)), 2048)) + + overcommit.append(min(self.clamp16(int(self.free * 0.26)), 2048)) + overcommit.append(min(self.clamp16(int(self.free * 0.3)), 2048)) + + # re-append current memorySize here, as recommended max might have changed + return list(sorted(list(set(default + [self.memorySize, self.recMax] + overcommit)))) + + @property + def readFactorOpts(self): + return list(sorted(list(set([1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5, 7, 10, 15, 20, 30, 50] + [self.readFactor])))) + + @property + def free(self): + return float(xbmc.getInfoLabel('System.Memory(free)')[:-2]) + + @property + def recMax(self): + freeMem = self.free + recMem = min(int(freeMem * self.safeFactor), 2048) + LOG("Free memory: {} MB, recommended max: {} MB".format(freeMem, recMem)) + return recMem + + +kcm = KodiCacheManager() + +CACHE_SIZE = kcm.memorySize + + +def T(ID, eng=''): + return ADDON.getLocalizedString(ID) + + +hasCustomBGColour = False +if KODI_VERSION_MAJOR > 18: + hasCustomBGColour = not advancedSettings.dynamicBackgrounds and advancedSettings.backgroundColour and \ + advancedSettings.backgroundColour != "-" + + +def getAdvancedSettings(): + # yes, global, hang me! + global advancedSettings + advancedSettings = AdvancedSettings() + + +def setSetting(key, value): + with SETTINGS_LOCK: + value = _processSettingForWrite(value) + ADDON.setSetting(key, value) + + +def _processSettingForWrite(value): + if isinstance(value, list): + value = binascii.hexlify(json.dumps(value)) + elif isinstance(value, bool): + value = value and 'true' or 'false' + return str(value) + + +def setGlobalProperty(key, val, base='script.plex.{0}'): + xbmcgui.Window(10000).setProperty(base.format(key), val) + + +def setGlobalBoolProperty(key, boolean, base='script.plex.{0}'): + xbmcgui.Window(10000).setProperty(base.format(key), boolean and '1' or '') + + +def getGlobalProperty(key): + return xbmc.getInfoLabel('Window(10000).Property(script.plex.{0})'.format(key)) + + +def showNotification(message, time_ms=3000, icon_path=None, header=ADDON.getAddonInfo('name')): + try: + icon_path = icon_path or translatePath(ADDON.getAddonInfo('icon')) + xbmc.executebuiltin('Notification({0},{1},{2},{3})'.format(header, message, time_ms, icon_path)) + except RuntimeError: # Happens when disabling the addon + LOG(message) + + +def videoIsPlaying(): + return xbmc.getCondVisibility('Player.HasVideo') + + +def messageDialog(heading='Message', msg=''): + from .windows import optionsdialog + optionsdialog.show(heading, msg, 'OK') + + +def showTextDialog(heading, text): + t = TextBox() + t.setControls(heading, text) + + +def sortTitle(title): + return title.startswith('The ') and title[4:] or title + + +def durationToText(seconds): + """ + Converts seconds to a short user friendly string + Example: 143 -> 2m 23s + """ + days = int(seconds / 86400000) + if days: + return '{0} day{1}'.format(days, days > 1 and 's' or '') + left = seconds % 86400000 + hours = int(left / 3600000) + if hours: + hours = '{0} hr{1} '.format(hours, hours > 1 and 's' or '') + else: + hours = '' + left = left % 3600000 + mins = int(left / 60000) + if mins: + return hours + '{0} min{1}'.format(mins, mins > 1 and 's' or '') + elif hours: + return hours.rstrip() + secs = int(left % 60000) + if secs: + secs /= 1000 + return '{0} sec{1}'.format(secs, secs > 1 and 's' or '') + return '0 seconds' + + +def durationToShortText(seconds): + """ + Converts seconds to a short user friendly string + Example: 143 -> 2m 23s + """ + days = int(seconds / 86400000) + if days: + return '{0} d'.format(days) + left = seconds % 86400000 + hours = int(left / 3600000) + if hours: + hours = '{0} h '.format(hours) + else: + hours = '' + left = left % 3600000 + mins = int(left / 60000) + if mins: + return hours + '{0} m'.format(mins) + elif hours: + return hours.rstrip() + secs = int(left % 60000) + if secs: + secs /= 1000 + return '{0} s'.format(secs) + return '0 s' + + +def cleanLeadingZeros(text): + if not text: + return '' + return re.sub(r'(?<= )0(\d)', r'\1', text) + + +def removeDups(dlist): + return [ii for n, ii in enumerate(dlist) if ii not in dlist[:n]] + + +SIZE_NAMES = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + + +def simpleSize(size): + """ + Converts bytes to a short user friendly string + Example: 12345 -> 12.06 KB + """ + s = 0 + if size > 0: + i = int(math.floor(math.log(size, 1024))) + p = math.pow(1024, i) + s = round(size / p, 2) + if (s > 0): + return '%s %s' % (s, SIZE_NAMES[i]) + else: + return '0B' + + +def timeDisplay(ms, cutHour=False): + h = ms / 3600000 + m = (ms % 3600000) / 60000 + s = (ms % 60000) / 1000 + if h >= 1 or not cutHour: + return '{0:0>2}:{1:0>2}:{2:0>2}'.format(int(h), int(m), int(s)) + return '{0:0>2}:{1:0>2}'.format(int(m), int(s)) + + +def simplifiedTimeDisplay(ms): + left, right = timeDisplay(ms).rsplit(':', 1) + left = left.lstrip('0:') or '0' + return left + ':' + right + + +def shortenText(text, size): + if len(text) < size: + return text + + return u'{0}\u2026'.format(text[:size - 1]) + + +def scaleResolution(w, h, by=None): + if by is None: + by = advancedSettings.posterResolutionScalePerc + + if 0 < by != 100.0: + px = w * h * (by / 100.0) + wratio = h / float(w) + hratio = w / float(h) + return int(round((px / wratio) ** .5)), int(round((px / hratio) ** .5)) + return w, h + + +class TextBox: + # constants + WINDOW = 10147 + CONTROL_LABEL = 1 + CONTROL_TEXTBOX = 5 + + def __init__(self, *args, **kwargs): + # activate the text viewer window + xbmc.executebuiltin("ActivateWindow(%d)" % (self.WINDOW, )) + # get window + self.win = xbmcgui.Window(self.WINDOW) + # give window time to initialize + xbmc.sleep(1000) + + def setControls(self, heading, text): + # set heading + self.win.getControl(self.CONTROL_LABEL).setLabel(heading) + # set text + self.win.getControl(self.CONTROL_TEXTBOX).setText(text) + + +class SettingControl: + def __init__(self, setting, log_display, disable_value=''): + self.setting = setting + self.logDisplay = log_display + self.disableValue = disable_value + self._originalMode = None + self.store() + + def disable(self): + rpc.Settings.SetSettingValue(setting=self.setting, value=self.disableValue) + DEBUG_LOG('{0}: DISABLED'.format(self.logDisplay)) + + def set(self, value): + rpc.Settings.SetSettingValue(setting=self.setting, value=value) + DEBUG_LOG('{0}: SET={1}'.format(self.logDisplay, value)) + + def store(self): + try: + self._originalMode = rpc.Settings.GetSettingValue(setting=self.setting).get('value') + DEBUG_LOG('{0}: Mode stored ({1})'.format(self.logDisplay, self._originalMode)) + except: + ERROR() + + def restore(self): + if self._originalMode is None: + return + rpc.Settings.SetSettingValue(setting=self.setting, value=self._originalMode) + DEBUG_LOG('{0}: RESTORED'.format(self.logDisplay)) + + @contextlib.contextmanager + def suspend(self): + self.disable() + yield + self.restore() + + @contextlib.contextmanager + def save(self): + yield + self.restore() + + +def timeInDayLocalSeconds(): + now = datetime.datetime.now() + sod = datetime.datetime(year=now.year, month=now.month, day=now.day) + sod = int(time.mktime(sod.timetuple())) + return int(time.time() - sod) + + +def getKodiSkipSteps(): + try: + return rpc.Settings.GetSettingValue(setting="videoplayer.seeksteps")["value"] + except: + return + + +def getKodiSlideshowInterval(): + try: + return rpc.Settings.GetSettingValue(setting="slideshow.staytime")["value"] + except: + return 3 + + +kodiSkipSteps = getKodiSkipSteps() +slideshowInterval = getKodiSlideshowInterval() + + +CRON = None + + +class CronReceiver(): + def tick(self): + pass + + def halfHour(self): + pass + + def day(self): + pass + + +class Cron(threading.Thread): + def __init__(self, interval): + threading.Thread.__init__(self, name='CRON') + self.stopped = threading.Event() + self.force = threading.Event() + self.interval = interval + self._lastHalfHour = self._getHalfHour() + self._receivers = [] + + global CRON + + CRON = self + + def __enter__(self): + self.start() + DEBUG_LOG('Cron started') + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.stop() + self.join() + + def _wait(self): + ct = 0 + while ct < self.interval: + xbmc.sleep(100) + ct += 0.1 + if self.force.isSet(): + self.force.clear() + return True + if MONITOR.abortRequested() or self.stopped.isSet(): + return False + return True + + def forceTick(self): + self.force.set() + + def stop(self): + self.stopped.set() + + def run(self): + while self._wait(): + self._tick() + DEBUG_LOG('Cron stopped') + + def _getHalfHour(self): + tid = timeInDayLocalSeconds() / 60 + return tid - (tid % 30) + + def _tick(self): + receivers = list(self._receivers) + receivers = self._halfHour(receivers) + for r in receivers: + try: + r.tick() + except: + ERROR() + + def _halfHour(self, receivers): + hh = self._getHalfHour() + if hh == self._lastHalfHour: + return receivers + try: + receivers = self._day(receivers, hh) + ret = [] + for r in receivers: + try: + if not r.halfHour(): + ret.append(r) + except: + ret.append(r) + ERROR() + return ret + finally: + self._lastHalfHour = hh + + def _day(self, receivers, hh): + if hh >= self._lastHalfHour: + return receivers + ret = [] + for r in receivers: + try: + if not r.day(): + ret.append(r) + except: + ret.append(r) + ERROR() + return ret + + def registerReceiver(self, receiver): + if receiver not in self._receivers: + DEBUG_LOG('Cron: Receiver added: {0}'.format(receiver)) + self._receivers.append(receiver) + + def cancelReceiver(self, receiver): + if receiver in self._receivers: + DEBUG_LOG('Cron: Receiver canceled: {0}'.format(receiver)) + self._receivers.pop(self._receivers.index(receiver)) + + +def getTimeFormat(): + """ + Generic: + Use locale.timeformat setting to get and make use of the format. + + Possible values: + HH:mm:ss -> %H:%M:%S + regional -> legacy + H:mm:ss -> %-H:%M:%S + + Legacy: Not necessarily true for Omega?; regional spices things up (depending on Kodi version?) + Get global time format. + Kodi's time format handling is weird, as they return incompatible formats for strftime. + %H%H can be returned for manually set zero-padded values, in case of a regional zero-padded hour component, + only %H is returned. + + For now, sail around that by testing the current time for padded hour values. + + Tests of the values returned by xbmc.getRegion("time") as of Kodi Nexus (I believe): + %I:%M:%S %p = h:mm:ss, non-zero-padded, 12h PM + %I:%M:%S = 12h, h:mm:ss, non-zero-padded, regional + %I%I:%M:%S = 12h, zero padded, hh:mm:ss + %H%H:%M:%S = 24h, zero padded, hh:mm:ss + %H:%M:%S = 24h, zero padded, regional, regional (central europe) + + :return: tuple of strftime-compatible format, boolean padHour + """ + + fmt = None + nonPadHF = "%-H" if sys.platform != "win32" else "%#H" + nonPadIF = "%-I" if sys.platform != "win32" else "%#I" + + try: + fmt = rpc.Settings.GetSettingValue(setting="locale.timeformat")["value"] + except: + DEBUG_LOG("Couldn't get locale.timeformat setting, falling back to legacy detection") + + if fmt and fmt != "regional": + # HH = padded 24h + # hh = padded 12h + # H = unpadded 24h + # h = unpadded 12h + + # handle non-padded hour first + if fmt.startswith("H:") or fmt.startswith("h:"): + adjustedFmt = fmt.replace("H", nonPadHF).replace("h", nonPadIF) + else: + adjustedFmt = fmt.replace("HH", "%H").replace("hh", "%I") + + padHour = adjustedFmt.startswith("%H") or adjustedFmt.startswith("%I") + + else: + DEBUG_LOG("Regional time format detected, falling back to legacy detection of hour-padding") + # regional is weirdly always unpadded (unless the broken %H%H/%I%I notation is used + origFmt = xbmc.getRegion('time') + + adjustedFmt = origFmt.replace("%H%H", "%H").replace("%I%I", "%I") + + # Checking for %H%H or %I%I only would be the obvious way here to determine whether the hour should be padded, + # but the formats returned for regional settings with padding might only have %H in them. + # Use a fallback (unreliable). + currentTime = xbmc.getInfoLabel('System.Time') + padHour = "%H%H" in origFmt or "%I%I" in origFmt or (currentTime[0] == "0" and currentTime[1] != ":") + + # Kodi Omega on Android seems to have borked the regional format returned separately + # (not happening on Windows at least). Format returned can be "%H:mm:ss", which is incompatible with strftime; fix. + adjustedFmt = adjustedFmt.replace("mm", "%M").replace("ss", "%S").replace("xx", "%p") + adjustedFmtKN = adjustedFmt.replace("%M", "mm").replace("%H", "hh").replace("%I", "h").replace("%S", "ss").\ + replace("%p", "xx").replace(nonPadIF, "h").replace(nonPadHF, "h") + + return adjustedFmt, adjustedFmtKN, padHour + + +timeFormat, timeFormatKN, padHour = getTimeFormat() + + +def populateTimeFormat(): + global timeFormat, timeFormatKN, padHour + timeFormat, timeFormatKN, padHour = getTimeFormat() + + +def getPlatform(): + for key in [ + 'System.Platform.Android', + 'System.Platform.Linux.RaspberryPi', + 'System.Platform.Linux', + 'System.Platform.Windows', + 'System.Platform.OSX', + 'System.Platform.IOS', + 'System.Platform.Darwin', + 'System.Platform.ATV2' + ]: + if xbmc.getCondVisibility(key): + return key.rsplit('.', 1)[-1] + + +def getProgressImage(obj): + if not obj.get('viewOffset') or not obj.get('duration'): + return '' + pct = int((obj.viewOffset.asInt() / obj.duration.asFloat()) * 100) + pct = pct - pct % 2 # Round to even number - we have even numbered progress only + pct = max(pct, 2) + return 'script.plex/progress/{0}.png'.format(pct) + + +def backgroundFromArt(art, width=1920, height=1080, background=colors.noAlpha.Background): + if not art: + return + return art.asTranscodedImageURL( + width, height, + blur=advancedSettings.backgroundArtBlurAmount2, + opacity=advancedSettings.backgroundArtOpacityAmount2, + background=background + ) + + +def trackIsPlaying(track): + return xbmc.getCondVisibility('String.StartsWith(MusicPlayer.Comment,{0})'.format('PLEX-{0}:'.format(track.ratingKey))) + + +def addURLParams(url, params): + if '?' in url: + url += '&' + else: + url += '?' + url += six.moves.urllib.parse.urlencode(params) + return url + + +OSS_CHUNK = 65536 + + +def getOpenSubtitlesHash(size, url): + long_long_format = "q" # long long + byte_size = struct.calcsize(long_long_format) + hash_ = filesize = size + if filesize < OSS_CHUNK * 2: + return + + buffer = b'' + for _range in ((0, OSS_CHUNK), (filesize-OSS_CHUNK, filesize)): + try: + r = requests.get(url, headers={"range": "bytes={0}-{1}".format(*_range)}, stream=True) + except: + return '' + buffer += r.raw.read(OSS_CHUNK) + + for x in range(int(OSS_CHUNK / byte_size) * 2): + size = x * byte_size + (l_value,) = struct.unpack(long_long_format, buffer[size:size + byte_size]) + hash_ += l_value + hash_ = hash_ & 0xFFFFFFFFFFFFFFFF + + return format(hash_, "016x") + + +def garbageCollect(): + gc.collect(2) + + +def shutdown(): + global MONITOR, ADDON, T, _SHUTDOWN + _SHUTDOWN = True + del MONITOR + del T + del ADDON diff --git a/script.plexmod/lib/windows/__init__.py b/script.plexmod/lib/windows/__init__.py new file mode 100644 index 000000000..f96b69008 --- /dev/null +++ b/script.plexmod/lib/windows/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +from . import kodigui +from lib import util + +kodigui.MONITOR = util.MONITOR diff --git a/script.plexmod/lib/windows/background.py b/script.plexmod/lib/windows/background.py new file mode 100644 index 000000000..8940d1c6b --- /dev/null +++ b/script.plexmod/lib/windows/background.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import +from . import kodigui +from lib import util + +util.setGlobalProperty('background.busy', '') +util.setGlobalProperty('background.shutdown', '') +util.setGlobalProperty('background.splash', '') + + +class BackgroundWindow(kodigui.BaseWindow): + xmlFile = 'script-plex-background.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + def __init__(self, *args, **kwargs): + kodigui.BaseWindow.__init__(self, *args, **kwargs) + self.function = kwargs.get('function') + + def onFirstInit(self): + self.function() + self.doClose() + + def onAction(self, action): + pass + + +def setBusy(on=True): + util.setGlobalProperty('background.busy', on and '1' or '') + + +def setSplash(on=True): + util.setGlobalProperty('background.splash', on and '1' or '') + + +def setShutdown(on=True): + util.setGlobalProperty('background.shutdown', on and '1' or '') + + +def killMonitor(): + kodigui.MONITOR = None diff --git a/script.plexmod/lib/windows/busy.py b/script.plexmod/lib/windows/busy.py new file mode 100644 index 000000000..1f128c5ec --- /dev/null +++ b/script.plexmod/lib/windows/busy.py @@ -0,0 +1,142 @@ +from __future__ import absolute_import +from . import kodigui +from lib import util +from kodi_six import xbmcgui +import threading + + +class BusyWindow(kodigui.BaseDialog): + xmlFile = 'script-plex-busy.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + +class BusyClosableWindow(BusyWindow): + ctx = None + + def onAction(self, action): + if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_STOP): + self.ctx.shouldClose = True + + +class BusyClosableMsgWindow(BusyClosableWindow): + xmlFile = 'script-plex-busy_msg.xml' + + def setMessage(self, msg): + self.setProperty("message", msg) + + +def dialog(msg='LOADING', condition=None, delay=True): + def methodWrap(func): + def inner(*args, **kwargs): + timer = None + w = BusyWindow.create(show=not delay) + + if delay: + timer = threading.Timer(0.5, w.show) + timer.start() + + try: + return func(*args, **kwargs) + finally: + if timer and timer.is_alive(): + timer.cancel() + timer.join() + del timer + w.doClose() + try: + del w + except: + pass + util.garbageCollect() + + if condition is not None: + return condition() and inner or func + return inner + + return methodWrap + + +def widthDialog(method, msg, *args, **kwargs): + return dialog(msg or 'LOADING')(method)(*args, **kwargs) + + +class BusyContext(object): + w = None + timer = None + shouldClose = False + window_cls = BusyWindow + delay = False + + def __enter__(self): + self.w = self.window_cls.create(show=not self.delay) + if self.delay: + self.timer = threading.Timer(0.5, lambda: self.w.show()) + self.w.ctx = self + return self + + def __exit__(self, exc_type, exc_value, tb): + if exc_type is not None: + util.ERROR() + + if self.timer and self.timer.is_alive(): + self.timer.cancel() + self.timer.join() + + self.w.doClose() + del self.w + self.w = None + util.garbageCollect() + return True + + +class BusyMsgContext(BusyContext): + window_cls = BusyClosableMsgWindow + + def setMessage(self, msg): + self.w.setMessage(msg) + + +class BusySignalContext(BusyMsgContext): + """ + Duplicates functionality of plex.CallbackEvent to a certain degree + """ + window_cls = BusyWindow + delay = True + + def __init__(self, context, signal, wait_max=10, delay=True): + self.wfSignal = signal + self.signalEmitter = context + self.waitMax = wait_max + self.ignoreSignal = False + self.signalReceived = False + self.delay = delay + + super(BusySignalContext, self).__init__() + + context.on(signal, self.onSignal) + + def onSignal(self, *args, **kwargs): + self.signalReceived = True + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + util.ERROR() + + try: + if not self.ignoreSignal: + waited = 0 + while not self.signalReceived and waited < self.waitMax: + util.MONITOR.waitForAbort(0.1) + waited += 0.1 + finally: + self.signalEmitter.off(self.wfSignal, self.onSignal) + + return super(BusySignalContext, self).__exit__(exc_type, exc_val, exc_tb) + + +class BusyClosableMsgContext(BusyMsgContext): + pass diff --git a/script.plexmod/lib/windows/currentplaylist.py b/script.plexmod/lib/windows/currentplaylist.py new file mode 100644 index 000000000..91dffd23a --- /dev/null +++ b/script.plexmod/lib/windows/currentplaylist.py @@ -0,0 +1,337 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from . import busy +from . import windowutils +from . import dropdown +from . import opener + +from lib import util +from lib import player +from lib import kodijsonrpc + +from lib.util import T + + +class CurrentPlaylistWindow(kodigui.ControlledWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-music_current_playlist.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + LI_THUMB_DIM = (64, 64) + ALBUM_THUMB_DIM = util.scaleResolution(639, 639) + + PLAYLIST_LIST_ID = 101 + + SEEK_BUTTON_ID = 500 + SEEK_IMAGE_ID = 510 + + POSITION_IMAGE_ID = 201 + SELECTION_INDICATOR = 202 + SELECTION_BOX = 203 + + REPEAT_BUTTON_ID = 401 + SHUFFLE_BUTTON_ID = 402 + SHUFFLE_REMOTE_BUTTON_ID = 422 + SKIP_PREV_BUTTON_ID = 404 + SKIP_NEXT_BUTTON_ID = 409 + PLAYLIST_BUTTON_ID = 410 + OPTIONS_BUTTON_ID = 411 + STOP_BUTTON_ID = 407 + + SEEK_IMAGE_WIDTH = 819 + SELECTION_BOX_WIDTH = 101 + SELECTION_INDICATOR_Y = 896 + + BAR_X = 0 + BAR_Y = 921 + BAR_RIGHT = 819 + BAR_BOTTOM = 969 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.selectedOffset = 0 + self.setDuration() + self.exitCommand = None + self.musicPlayerWinID = kwargs.get('winID') + + def doClose(self, **kwargs): + player.PLAYER.off('playback.started', self.onPlayBackStarted) + player.PLAYER.off('playlist.changed', self.playQueueCallback) + if player.PLAYER.handler.playQueue and player.PLAYER.handler.playQueue.isRemote: + player.PLAYER.handler.playQueue.off('change', self.updateProperties) + kodigui.ControlledWindow.doClose(self) + + def onFirstInit(self): + self.playlistListControl = kodigui.ManagedControlList(self, self.PLAYLIST_LIST_ID, 9) + self.setupSeekbar() + + self.fillPlaylist() + self.selectPlayingItem() + self.setFocusId(self.PLAYLIST_LIST_ID) + + self.updateProperties() + if player.PLAYER.handler.playQueue and player.PLAYER.handler.playQueue.isRemote: + player.PLAYER.handler.playQueue.on('change', self.updateProperties) + player.PLAYER.on('playlist.changed', self.playQueueCallback) + + def onAction(self, action): + try: + controlID = self.getFocusId() + if self.checkSeekActions(action, controlID): + return + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.PLAYLIST_LIST_ID: + self.playlistListClicked() + elif controlID == self.SEEK_BUTTON_ID: + self.seekButtonClicked() + elif controlID == self.SHUFFLE_BUTTON_ID: + self.fillPlaylist() + elif controlID == self.SHUFFLE_REMOTE_BUTTON_ID: + player.PLAYER.handler.playQueue.setShuffle() + elif controlID == self.REPEAT_BUTTON_ID: + self.repeatButtonClicked() + elif controlID == self.SKIP_PREV_BUTTON_ID: + self.skipPrevButtonClicked() + elif controlID == self.SKIP_NEXT_BUTTON_ID: + self.skipNextButtonClicked() + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked() + elif controlID == self.STOP_BUTTON_ID: + self.stopButtonClicked() + + def onFocus(self, controlID): + if controlID == self.SEEK_BUTTON_ID: + try: + if player.PLAYER.isPlaying(): + self.selectedOffset = player.PLAYER.getTime() * 1000 + else: + self.selectedOffset = 0 + except RuntimeError: + self.selectedOffset = 0 + + self.updateSelectedProgress() + + def onPlayBackStarted(self, **kwargs): + xbmc.sleep(2000) + self.setDuration() + + def repeatButtonClicked(self): + if player.PLAYER.handler.playQueue and player.PLAYER.handler.playQueue.isRemote: + if xbmc.getCondVisibility('Playlist.IsRepeatOne'): + xbmc.executebuiltin('PlayerControl(RepeatOff)') + elif player.PLAYER.handler.playQueue.isRepeat: + player.PLAYER.handler.playQueue.setRepeat(False) + player.PLAYER.handler.playQueue.refresh(force=True) + xbmc.executebuiltin('PlayerControl(RepeatOne)') + else: + player.PLAYER.handler.playQueue.setRepeat(True) + player.PLAYER.handler.playQueue.refresh(force=True) + else: + xbmc.executebuiltin('PlayerControl(Repeat)') + + def skipPrevButtonClicked(self): + if not xbmc.getCondVisibility('MusicPlayer.HasPrevious') and player.PLAYER.handler.playQueue and player.PLAYER.handler.playQueue.isRemote: + util.DEBUG_LOG('MusicPlayer: No previous in Kodi playlist - refreshing remote PQ') + if not player.PLAYER.handler.playQueue.refresh(force=True, wait=True): + return + + xbmc.executebuiltin('PlayerControl(Previous)') + + def skipNextButtonClicked(self): + if not xbmc.getCondVisibility('MusicPlayer.HasNext') and player.PLAYER.handler.playQueue and player.PLAYER.handler.playQueue.isRemote: + util.DEBUG_LOG('MusicPlayer: No next in Kodi playlist - refreshing remote PQ') + if not player.PLAYER.handler.playQueue.refresh(force=True, wait=True): + return + + xbmc.executebuiltin('PlayerControl(Next)') + + def optionsButtonClicked(self, pos=(670, 1060)): + track = player.PLAYER.currentTrack() + if not track: + return + + options = [] + + options.append({'key': 'to_album', 'display': T(32300, 'Go to Album')}) + options.append({'key': 'to_artist', 'display': T(32301, 'Go to Artist')}) + options.append({'key': 'to_section', 'display': T(32302, u'Go to {0}').format(track.getLibrarySectionTitle())}) + + choice = dropdown.showDropdown(options, pos, close_direction='down', pos_is_bottom=True, close_on_playback_ended=True) + if not choice: + return + + if choice['key'] == 'to_album': + self.processCommand(opener.open(track.parentRatingKey)) + elif choice['key'] == 'to_artist': + self.processCommand(opener.open(track.grandparentRatingKey)) + elif choice['key'] == 'to_section': + self.goHome(track.getLibrarySectionId()) + + def stopButtonClicked(self): + xbmc.executebuiltin('Action(Back, {})'.format(self.musicPlayerWinID)) + xbmc.sleep(500) + self.doClose() + + def selectPlayingItem(self): + for mli in self.playlistListControl: + if xbmc.getCondVisibility('String.StartsWith(MusicPlayer.Comment,{0})'.format(mli.dataSource['comment'].split(':', 1)[0])): + self.playlistListControl.selectItem(mli.pos()) + break + + def playQueueCallback(self, **kwargs): + self.setProperty('pq.isshuffled', player.PLAYER.handler.playQueue.isShuffled and '1' or '') + mli = self.playlistListControl.getSelectedItem() + pi = mli.dataSource + plexID = pi['comment'].split(':', 1)[0] + viewPos = self.playlistListControl.getViewPosition() + + self.fillPlaylist() + + for ni in self.playlistListControl: + if ni.dataSource['comment'].split(':', 1)[0] == plexID: + self.playlistListControl.selectItem(ni.pos()) + break + + xbmc.sleep(100) + + newViewPos = self.playlistListControl.getViewPosition() + if viewPos != newViewPos: + diff = newViewPos - viewPos + self.playlistListControl.shiftView(diff, True) + + def seekButtonClicked(self): + player.PLAYER.seekTime(self.selectedOffset / 1000.0) + + def playlistListClicked(self): + mli = self.playlistListControl.getSelectedItem() + if not mli: + return + player.PLAYER.playselected(mli.pos()) + + def createListItem(self, pi, idx): + label2 = '{0} / {1}'.format(pi['artist'][0], pi['album']) + plexInfo = pi['comment'] + mli = kodigui.ManagedListItem(pi['title'], label2, thumbnailImage=pi['thumbnail'], data_source=pi) + mli.setProperty('track.duration', util.simplifiedTimeDisplay(pi['duration'] * 1000)) + if plexInfo.startswith('PLEX-'): + mli.setProperty('track.ID', plexInfo.split('-', 1)[-1].split(':', 1)[0]) + mli.setProperty('track.number', str(pi['playcount'])) + else: + mli.setProperty('track.ID', '!NONE!') + mli.setProperty('track.number', str(pi['track'])) + mli.setProperty('playlist.position', str(idx)) + + mli.setProperty('file', pi['file']) + return mli + + @busy.dialog() + def fillPlaylist(self): + items = [] + idx = 1 + for pi in kodijsonrpc.rpc.PlayList.GetItems( + playlistid=xbmc.PLAYLIST_MUSIC, properties=['title', 'artist', 'album', 'track', 'thumbnail', 'duration', 'playcount', 'comment', 'file'] + )['items']: + mli = self.createListItem(pi, idx) + if mli: + mli.setProperty('index', str(idx)) + items.append(mli) + idx += 1 + + self.playlistListControl.reset() + self.playlistListControl.addItems(items) + + def setupSeekbar(self): + self.seekbarControl = self.getControl(self.SEEK_IMAGE_ID) + self.selectionIndicator = self.getControl(self.SELECTION_INDICATOR) + self.selectionBox = self.getControl(self.SELECTION_BOX) + self.selectionBoxHalf = self.SELECTION_BOX_WIDTH // 2 + self.selectionBoxMax = self.SEEK_IMAGE_WIDTH + player.PLAYER.on('playback.started', self.onPlayBackStarted) + + def checkSeekActions(self, action, controlID): + if controlID == self.SEEK_BUTTON_ID: + if action == xbmcgui.ACTION_MOUSE_MOVE: + self.seekMouse(action) + return True + elif action in (xbmcgui.ACTION_MOVE_RIGHT, xbmcgui.ACTION_NEXT_ITEM): + self.seekForward(3000) + return True + elif action in (xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_PREV_ITEM): + self.seekBack(3000) + return True + # elif action == xbmcgui.ACTION_MOVE_UP: + # self.seekForward(60000) + # elif action == xbmcgui.ACTION_MOVE_DOWN: + # self.seekBack(60000) + elif action == xbmcgui.ACTION_STOP: + self.stopButtonClicked() + return True + + def setDuration(self): + try: + self.duration = player.PLAYER.getTotalTime() * 1000 + except RuntimeError: # Not playing + self.duration = 0 + + def seekForward(self, offset): + self.selectedOffset += offset + if self.selectedOffset > self.duration: + self.selectedOffset = self.duration + + self.updateSelectedProgress() + + def seekBack(self, offset): + self.selectedOffset -= offset + if self.selectedOffset < 0: + self.selectedOffset = 0 + + self.updateSelectedProgress() + + def seekMouse(self, action): + x = self.mouseXTrans(action.getAmount1()) + y = self.mouseYTrans(action.getAmount2()) + if not (self.BAR_Y <= y <= self.BAR_BOTTOM): + return + + if not (self.BAR_X <= x <= self.BAR_RIGHT): + return + + self.selectedOffset = int((x - self.BAR_X) / float(self.SEEK_IMAGE_WIDTH) * self.duration) + self.updateSelectedProgress() + + def updateSelectedProgress(self): + ratio = self.selectedOffset / float(self.duration) + w = int(ratio * self.SEEK_IMAGE_WIDTH) + self.seekbarControl.setWidth(w or 1) + + self.selectionIndicator.setPosition(w, self.SELECTION_INDICATOR_Y) + if w < self.selectionBoxHalf - 3: + self.selectionBox.setPosition((-self.selectionBoxHalf + (self.selectionBoxHalf - w)) - 3, 0) + elif w > self.selectionBoxMax: + self.selectionBox.setPosition((-self.SELECTION_BOX_WIDTH + (self.SEEK_IMAGE_WIDTH - w)) + 3, 0) + else: + self.selectionBox.setPosition(-self.selectionBoxHalf, 0) + self.setProperty('time.selection', util.simplifiedTimeDisplay(int(self.selectedOffset))) + + def updateProperties(self, **kwargs): + pq = player.PLAYER.handler.playQueue + if pq: + if pq.isRemote: + self.setProperty('pq.isRemote', '1') + self.setProperty('pq.hasnext', pq.allowSkipNext and '1' or '') + self.setProperty('pq.hasprev', pq.allowSkipPrev and '1' or '') + self.setProperty('pq.repeat', pq.isRepeat and '1' or '') + self.setProperty('pq.shuffled', pq.isShuffled and '1' or '') + else: + self.setProperties(('pq.isRemote', 'pq.hasnext', 'pq.hasprev', 'pq.repeat', 'pq.shuffled'), '') diff --git a/script.plexmod/lib/windows/dropdown.py b/script.plexmod/lib/windows/dropdown.py new file mode 100644 index 000000000..dc973b02b --- /dev/null +++ b/script.plexmod/lib/windows/dropdown.py @@ -0,0 +1,247 @@ +from __future__ import absolute_import +from kodi_six import xbmc, xbmcgui +from . import kodigui + +from lib import util + +SEPARATOR = None + + +class DropdownDialog(kodigui.BaseDialog): + xmlFile = 'script-plex-dropdown.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + GROUP_ID = 100 + OPTIONS_LIST_ID = 250 + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + self.options = kwargs.get('options') + self.pos = kwargs.get('pos') + self.lastSelectedItem = None + self.optionsList = None + self.roundRobin = kwargs.get('round_robin', True) + self.posIsBottom = kwargs.get('pos_is_bottom') + self.closeDirection = kwargs.get('close_direction') + self.setDropdownProp = kwargs.get('set_dropdown_prop', False) + self.withIndicator = kwargs.get('with_indicator', False) + self.suboptionCallback = kwargs.get('suboption_callback') + self.closeOnPlaybackEnded = kwargs.get('close_on_playback_ended', False) + self.closeOnlyWithBack = kwargs.get('close_only_with_back', False) + self.alignItems = kwargs.get('align_items', 'center') + self.optionsCallback = kwargs.get('options_callback', None) + self.header = kwargs.get('header') + self.selectIndex = kwargs.get('select_index') + self.onCloseCallback = kwargs.get('onclose_callback') + self.choice = None + + @property + def x(self): + return min(self.width - 360, self.pos[0]) + + @property + def y(self): + y = self.pos[1] + if self.posIsBottom: + y -= (len(self.options) * 66) + 80 + return y + + def onFirstInit(self): + self.setProperty('dropdown', self.setDropdownProp and '1' or '') + self.setProperty('header', self.header) + self.optionsList = kodigui.ManagedControlList(self, self.OPTIONS_LIST_ID, 8) + self.showOptions() + height = min(66 * 14, (len(self.options) * 66)) + 80 + self.getControl(100).setPosition(self.x, self.y) + + shadowControl = self.getControl(110) + if self.header: + shadowControl.setHeight(height + 86) + self.getControl(111).setHeight(height + 6) + else: + shadowControl.setHeight(height) + + self.setProperty('show', '1') + self.setProperty('close.direction', self.closeDirection) + if self.closeOnPlaybackEnded: + from lib import player + player.PLAYER.on('session.ended', self.playbackSessionEnded) + + def onAction(self, action): + try: + pass + except: + util.ERROR() + + if self.roundRobin and action in (xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_MOVE_DOWN) and \ + self.getFocusId() == self.OPTIONS_LIST_ID: + to_pos = None + last_index = self.optionsList.size() - 1 + + if last_index > 0: + if action == xbmcgui.ACTION_MOVE_UP and self.lastSelectedItem in (0, None) \ + and self.optionsList.topHasFocus(): + to_pos = last_index + + elif action == xbmcgui.ACTION_MOVE_DOWN and self.lastSelectedItem == last_index \ + and self.optionsList.bottomHasFocus(): + to_pos = 0 + + if to_pos is not None: + self.optionsList.setSelectedItemByPos(to_pos) + self.lastSelectedItem = to_pos + return + + self.lastSelectedItem = self.optionsList.control.getSelectedPosition() + + kodigui.BaseDialog.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.OPTIONS_LIST_ID: + self.setChoice() + else: + self.doClose() + + def playbackSessionEnded(self, **kwargs): + self.doClose() + + def doClose(self): + if self.closeOnPlaybackEnded: + from lib import player + player.PLAYER.off('session.ended', self.playbackSessionEnded) + + self.optionsList.reset() + self.optionsList = None + + self.setProperty('show', '') + + super(DropdownDialog, self).doClose() + + def onClosed(self): + if self.onCloseCallback: + self.onCloseCallback(self.choice) + + def setChoice(self): + mli = self.optionsList.getSelectedItem() + if not mli: + return + + choice = self.options[self.optionsList.getSelectedPosition()] + + if choice.get('ignore'): + return + + if self.suboptionCallback: + options = self.suboptionCallback(choice) + if options: + sub = showDropdown(options, (self.x + 290, self.y + 10), close_direction='left', with_indicator=True) + if not sub: + return + + choice['sub'] = sub + + self.choice = choice + if self.optionsCallback: + self.optionsCallback(self.optionsList, mli) + + del mli + + if not self.closeOnlyWithBack: + self.doClose() + + def showOptions(self): + items = [] + options = [] + for oo in self.options: + if oo: + o = oo.copy() + item = kodigui.ManagedListItem(o['display'], thumbnailImage=o.get('indicator', ''), data_source=o) + item.setProperty('with.indicator', self.withIndicator and '1' or '') + item.setProperty('align', self.alignItems) + items.append(item) + options.append(o) + else: + if items: + items[-1].setProperty('separator', '1') + + self.options = options + + if len(items) > 1: + items[0].setProperty('first', '1') + items[-1].setProperty('last', '1') + elif items: + items[0].setProperty('only', '1') + + self.optionsList.reset() + self.optionsList.addItems(items) + + self.setFocusId(self.OPTIONS_LIST_ID) + + if self.selectIndex is not None: + self.optionsList.setSelectedItemByPos(self.selectIndex) + self.lastSelectedItem = self.selectIndex + + +class DropdownHeaderDialog(DropdownDialog): + xmlFile = 'script-plex-dropdown_header.xml' + + +def showDropdown( + options, pos=None, + pos_is_bottom=False, + close_direction='top', + set_dropdown_prop=True, + with_indicator=False, + suboption_callback=None, + close_on_playback_ended=False, + close_only_with_back=False, + align_items='center', + options_callback=None, + header=None, + select_index=None, + onclose_callback=None, +): + + if header: + pos = pos or (660, 400) + w = DropdownHeaderDialog.open( + options=options, pos=pos, + pos_is_bottom=pos_is_bottom, + close_direction=close_direction, + set_dropdown_prop=set_dropdown_prop, + with_indicator=with_indicator, + suboption_callback=suboption_callback, + close_on_playback_ended=close_on_playback_ended, + close_only_with_back=close_only_with_back, + align_items=align_items, + options_callback=options_callback, + header=header, + select_index=select_index, + onclose_callback=onclose_callback, + ) + else: + pos = pos or (810, 400) + w = DropdownDialog.open( + options=options, pos=pos, + pos_is_bottom=pos_is_bottom, + close_direction=close_direction, + set_dropdown_prop=set_dropdown_prop, + with_indicator=with_indicator, + suboption_callback=suboption_callback, + close_on_playback_ended=close_on_playback_ended, + close_only_with_back=close_only_with_back, + align_items=align_items, + options_callback=options_callback, + header=header, + select_index=select_index, + onclose_callback=onclose_callback, + ) + choice = w.choice + w = None + del w + util.garbageCollect() + return choice diff --git a/script.plexmod/lib/windows/episodes.py b/script.plexmod/lib/windows/episodes.py new file mode 100644 index 000000000..65af08754 --- /dev/null +++ b/script.plexmod/lib/windows/episodes.py @@ -0,0 +1,1202 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from lib import util +from lib import backgroundthread +from lib import metadata +from lib import player + +from plexnet import plexapp, playlist, plexplayer +from plexnet.util import INTERFACE + +from . import busy +from . import videoplayer +from . import dropdown +from . import windowutils +from . import opener +from . import search +from . import playersettings +from . import info +from . import optionsdialog +from . import preplayutils +from . import pagination +from . import playbacksettings + +from lib.util import T +from .mixins import SeasonsMixin + +VIDEO_RELOAD_KW = dict(includeExtras=1, includeExtrasCount=10, includeChapters=1) + + +class EpisodeReloadTask(backgroundthread.Task): + def setup(self, episode, callback, with_progress=False): + self.episode = episode + self.callback = callback + self.withProgress = with_progress + return self + + def run(self): + if self.isCanceled(): + return + + if not plexapp.SERVERMANAGER.selectedServer: + # Could happen during sign-out for instance + return + + try: + self.episode.reload(checkFiles=1, includeChapters=1, fromMediaChoice=self.episode.mediaChoice is not None) + if self.isCanceled(): + return + self.callback(self, self.episode, with_progress=self.withProgress) + except: + util.ERROR() + + +class EpisodesPaginator(pagination.MCLPaginator): + thumbFallback = 'script.plex/thumb_fallbacks/show.png' + _currentEpisode = None + + def reset(self): + super(EpisodesPaginator, self).reset() + self._currentEpisode = None + + def getData(self, offset, amount): + return (self.parentWindow.season or self.parentWindow.show_).episodes(offset=offset, limit=amount) + + def createListItem(self, data): + mli = super(EpisodesPaginator, self).createListItem(data) + self.parentWindow.setItemInfo(data, mli) + return mli + + def prepareListItem(self, data, mli): + if not mli.dataSource.isWatched: + mli.setProperty('unwatched.count', str(mli.dataSource.unViewedLeafCount)) + mli.setProperty('progress', util.getProgressImage(mli.dataSource)) + + def setEpisode(self, ep): + self._currentEpisode = ep + + @property + def initialPage(self): + episode = self.parentWindow.episode + offset = 0 + amount = self.initialPageSize + if episode: + self.setEpisode(episode) + # try cutting the query short while not querying all episodes, to find the slice with the currently + # selected episode in it + episodes = [] + _amount = self.initialPageSize + self.orphans + epSeasonIndex = int(episode.index or 1) - 1 # .index is 1-based + if _amount < self.leafCount: + _amount = self.initialPageSize * 2 + notFound = False + while episode not in episodes: + offset = int(max(0, epSeasonIndex - _amount / 2)) + episodes = self.getData(offset, int(_amount)) + + if _amount >= self.leafCount: + # ep not found? + notFound = True + break + + # in case the episode wasn't found inside the slice, increase the slice's size + _amount *= 2 + + if notFound: + # search conservatively + util.DEBUG_LOG("Episode not found with intelligent index-based search, re-trying conservatively") + _amount = self.initialPageSize * 2 + offset = 0 + episodes = self.getData(offset, int(_amount)) + while episode not in episodes: + offset = _amount + episodes = self.getData(offset, int(_amount)) + + if _amount >= self.leafCount: + break + + _amount *= 2 + else: + # shortcut for short seasons + episodes = self.getData(offset, int(_amount)) + + else: + return super(EpisodesPaginator, self).initialPage + + episodeFound = episode and episode in episodes + if episodeFound: + if self.initialPageSize + self.orphans < self.leafCount: + # slice around the episode + # Clamp the left side dynamically based on the item index and how many items are left in the season. + # The episodes list might be longer than our limit, because the season doesn't necessarily have all the + # episodes in it and we're basing the initial load on the current episode's index, which is the actual + # index of the episode in the season, not what's physically there. To find the episode, we're + # dynamically increasing the window size above. Re-clamp to :amount:, adding slack to both sides if + # the remaining episodes would fit inside half of :amount:. + tmpEpIdx = episodes.index(episode) + leftBoundary = self.initialPageSize - len(episodes[tmpEpIdx:tmpEpIdx + self.orphans]) + + left = max(tmpEpIdx - leftBoundary, 0) + offset += left + epsLeft = self.leafCount - offset + # avoid short pages on the right end + if epsLeft <= self.initialPageSize + self.orphans: + amount = epsLeft + + # avoid short pages on the left end + if offset < self.orphans and amount + offset < self.initialPageSize + self.orphans: + amount += offset + left = 0 + offset = 0 + + episodes = episodes[left:left + amount] + + self.offset = offset + self._currentAmount = len(episodes) + + return episodes + + def selectItem(self, amount, more_left=False, more_right=False, items=None): + if not super(EpisodesPaginator, self).selectItem(amount, more_left): + if (self._currentEpisode and items) and self._currentEpisode in items: + self.control.selectItem(items.index(self._currentEpisode) + (1 if more_left else 0)) + + +class RelatedPaginator(pagination.BaseRelatedPaginator): + def getData(self, offset, amount): + return self.parentWindow.show_.getRelated(offset=offset, limit=amount) + + +class EpisodesWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMixin, + playbacksettings.PlaybackSettingsMixin): + xmlFile = 'script-plex-episodes.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + THUMB_AR16X9_DIM = util.scaleResolution(657, 393) + POSTER_DIM = util.scaleResolution(420, 630) + RELATED_DIM = util.scaleResolution(268, 397) + EXTRA_DIM = util.scaleResolution(329, 185) + ROLES_DIM = util.scaleResolution(334, 334) + + LIST_OPTIONS_BUTTON_ID = 111 + + EPISODE_LIST_ID = 400 + SEASONS_LIST_ID = 401 + EXTRA_LIST_ID = 402 + RELATED_LIST_ID = 403 + ROLES_LIST_ID = 404 + + OPTIONS_GROUP_ID = 200 + + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + PLAYER_STATUS_BUTTON_ID = 204 + + PROGRESS_IMAGE_ID = 250 + + PLAY_BUTTON_ID = 301 + SHUFFLE_BUTTON_ID = 302 + OPTIONS_BUTTON_ID = 303 + INFO_BUTTON_ID = 304 + SETTINGS_BUTTON_ID = 305 + MEDIA_BUTTON_ID = 307 + + SEASONS_CONTROL_ATTR = "seasonsListControl" + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + windowutils.UtilMixin.__init__(self) + self.episode = None + self.reset(kwargs.get('episode'), kwargs.get('season'), kwargs.get('show')) + self.initialEpisode = kwargs.get('episode') + self.parentList = kwargs.get('parentList') + self.lastItem = None + self.lastFocusID = None + self.lastNonOptionsFocusID = None + self.episodesPaginator = None + self.relatedPaginator = None + self.cameFrom = kwargs.get('came_from') + self.tasks = backgroundthread.Tasks() + self.initialized = False + self.closing = False + self._reloadVideos = [] + + def reset(self, episode, season=None, show=None): + self.episode = episode + self.season = season if season is not None else self.episode.season() + try: + self.show_ = show or (self.episode or self.season).show().reload(includeExtras=1, includeExtrasCount=10) + except IndexError: + raise util.NoDataException + + self.parentList = None + self.seasons = None + self._reloadVideos = [] + #self.initialized = False + + def doClose(self): + self.closing = True + self.episodesPaginator = None + self.relatedPaginator = None + kodigui.ControlledWindow.doClose(self) + if self.tasks: + self.tasks.cancel() + self.tasks = None + try: + player.PLAYER.off('new.video', self.onNewVideo) + except KeyError: + pass + + @busy.dialog() + def _onFirstInit(self): + self.episodeListControl = kodigui.ManagedControlList(self, self.EPISODE_LIST_ID, 5) + self.progressImageControl = self.getControl(self.PROGRESS_IMAGE_ID) + + self.extraListControl = kodigui.ManagedControlList(self, self.EXTRA_LIST_ID, 5) + self.relatedListControl = kodigui.ManagedControlList(self, self.RELATED_LIST_ID, 5) + self.rolesListControl = kodigui.ManagedControlList(self, self.ROLES_LIST_ID, 5) + self.seasonsListControl = kodigui.ManagedControlList(self, self.SEASONS_LIST_ID, 5) + + self._setup() + self.postSetup() + + def doAutoPlay(self): + # First reload the video to get all the other info + self.initialEpisode.reload(checkFiles=1, **VIDEO_RELOAD_KW) + return self.playButtonClicked(force_episode=self.initialEpisode) + + def onFirstInit(self): + self._onFirstInit() + + if self.show_ and self.show_.theme and not util.getSetting("slow_connection", False) and \ + (not self.cameFrom or self.cameFrom != self.show_.ratingKey): + volume = self.show_.settings.getThemeMusicValue() + if volume > 0: + player.PLAYER.playBackgroundMusic(self.show_.theme.asURL(True), volume, + self.show_.ratingKey) + + @busy.dialog() + def onReInit(self): + if not self.tasks: + self.tasks = backgroundthread.Tasks() + + try: + self.selectEpisode() + except AttributeError: + raise util.NoDataException + + mli = self.episodeListControl.getSelectedItem() + if not mli or not self.episodesPaginator: + return + + reloadItems = [mli] + for v in self._reloadVideos: + for m in self.episodeListControl: + if m.dataSource == v: + reloadItems.append(m) + self.episodesPaginator.prepareListItem(v, m) + + # re-set current item's progress to a loading state + if util.getSetting("slow_connection", False): + self.progressImageControl.setWidth(1) + mli.setProperty('remainingTime', T(32914, "Loading")) + + self.reloadItems(items=reloadItems, with_progress=True) + self.episodesPaginator.setEpisode(self._reloadVideos and self._reloadVideos[-1] or mli) + self._reloadVideos = [] + self.fillRelated() + + def postSetup(self, from_select_episode=False): + self.selectEpisode(from_select_episode=from_select_episode) + self.checkForHeaderFocus(xbmcgui.ACTION_MOVE_DOWN) + self.setFocusId(self.PLAY_BUTTON_ID) + self.initialized = True + + @busy.dialog() + def setup(self): + self._setup() + + def _setup(self, from_select_episode=False): + player.PLAYER.on('new.video', self.onNewVideo) + (self.season or self.show_).reload(checkFiles=1, **VIDEO_RELOAD_KW) + + if not from_select_episode or not self.episodesPaginator: + self.episodesPaginator = EpisodesPaginator(self.episodeListControl, + leaf_count=int(self.season.leafCount) if self.season else 0, + parent_window=self) + + if not from_select_episode or not self.episodesPaginator: + self.relatedPaginator = RelatedPaginator(self.relatedListControl, leaf_count=int(self.show_.relatedCount), + parent_window=self) + + self.updateProperties() + self.setBoolProperty("initialized", True) + self.fillEpisodes() + hasSeasons = self.fillSeasons(self.show_, seasonsFilter=lambda x: len(x) > 1, selectSeason=self.season) + hasPrev = self.fillExtras(hasSeasons) + + if not hasPrev and hasSeasons: + hasPrev = True + hasPrev = self.fillRelated(hasPrev) + self.fillRoles(hasPrev) + + def selectEpisode(self, from_select_episode=False): + if not self.episode: + return + + for mli in self.episodeListControl: + if mli.dataSource == self.episode: + self.episodeListControl.selectItem(mli.pos()) + self.episodesPaginator.setEpisode(self.episode) + break + else: + if not from_select_episode: + self.reset(self.episode) + self._setup(from_select_episode=True) + self.postSetup(from_select_episode=True) + + self.episode = None + + def onAction(self, action): + try: + controlID = self.getFocusId() + + if not controlID and self.lastFocusID and not action == xbmcgui.ACTION_MOUSE_MOVE: + self.setFocusId(self.lastFocusID) + + if action == xbmcgui.ACTION_LAST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + next(self) + elif action == xbmcgui.ACTION_NEXT_ITEM: + next(self) + elif action == xbmcgui.ACTION_FIRST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + self.prev() + elif action == xbmcgui.ACTION_PREV_ITEM: + self.prev() + + if action == xbmcgui.ACTION_MOVE_UP and controlID in (self.EPISODE_LIST_ID, self.SEASONS_LIST_ID): + self.updateBackgroundFrom((self.show_ or self.season.show())) + + if controlID == self.EPISODE_LIST_ID: + if self.checkForHeaderFocus(action): + return + + elif controlID == self.RELATED_LIST_ID: + if self.relatedPaginator.boundaryHit: + self.relatedPaginator.paginate() + return + elif action in (xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT): + self.updateBackgroundFrom(self.relatedListControl.getSelectedItem().dataSource) + + if controlID == self.LIST_OPTIONS_BUTTON_ID and self.checkOptionsAction(action): + return + elif action == xbmcgui.ACTION_CONTEXT_MENU: + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + self.lastNonOptionsFocusID = self.lastFocusID + self.setFocusId(self.OPTIONS_GROUP_ID) + return + else: + if self.lastNonOptionsFocusID: + self.setFocusId(self.lastNonOptionsFocusID) + self.lastNonOptionsFocusID = None + return + + elif action == xbmcgui.ACTION_NAV_BACK: + if (not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format( + self.OPTIONS_GROUP_ID)) or not controlID) and \ + not util.advancedSettings.fastBack: + if self.getProperty('on.extras'): + self.setFocusId(self.OPTIONS_GROUP_ID) + return + + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU): + self.doClose() + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def onNewVideo(self, video=None, **kwargs): + if not video: + return + + if not video.type == 'episode': + return + + util.DEBUG_LOG('Updating selected episode: {0}'.format(video)) + self.episode = video + self._reloadVideos.append(video) + + return True + + def checkOptionsAction(self, action): + if action == xbmcgui.ACTION_MOVE_UP: + mli = self.episodeListControl.getSelectedItem() + if not mli or mli.getProperty("is.boundary"): + return False + pos = mli.pos() - 1 + if self.episodeListControl.positionIsValid(pos): + self.setFocusId(self.EPISODE_LIST_ID) + self.episodeListControl.selectItem(pos) + return True + elif action == xbmcgui.ACTION_MOVE_DOWN: + mli = self.episodeListControl.getSelectedItem() + if not mli or mli.getProperty("is.boundary"): + return False + pos = mli.pos() + 1 + if self.episodeListControl.positionIsValid(pos): + self.setFocusId(self.EPISODE_LIST_ID) + self.episodeListControl.selectItem(pos) + return True + + return False + + def onClick(self, controlID): + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.EPISODE_LIST_ID: + self.episodeListClicked() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.PLAY_BUTTON_ID: + self.playButtonClicked() + elif controlID == self.SHUFFLE_BUTTON_ID: + self.shuffleButtonClicked() + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked() + elif controlID == self.SETTINGS_BUTTON_ID: + self.settingsButtonClicked() + elif controlID == self.MEDIA_BUTTON_ID: + self.mediaButtonClicked() + elif controlID == self.INFO_BUTTON_ID: + self.infoButtonClicked() + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + elif controlID == self.SEASONS_LIST_ID: + mli = self.seasonsListControl.getSelectedItem() + if not mli: + return + item = mli.dataSource + if item != self.season: + self.openItem(self.seasonsListControl, came_from=self.season.parentRatingKey) + else: + self.setFocusId(self.EPISODE_LIST_ID) + elif controlID == self.EXTRA_LIST_ID: + self.openItem(self.extraListControl) + elif controlID == self.RELATED_LIST_ID: + self.openItem(self.relatedListControl) + elif controlID == self.ROLES_LIST_ID: + self.roleClicked() + + def onFocus(self, controlID): + self.lastFocusID = controlID + + if 399 < controlID < 500: + self.setProperty('hub.focus', str(controlID - 400)) + if controlID == self.RELATED_LIST_ID: + self.updateBackgroundFrom(self.relatedListControl.getSelectedItem().dataSource) + if xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + ControlGroup(300).HasFocus(0)'): + self.setProperty('on.extras', '') + elif xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + !ControlGroup(300).HasFocus(0)'): + self.setProperty('on.extras', '1') + + if player.PLAYER.bgmPlaying and player.PLAYER.handler.currentlyPlaying != self.season.show().ratingKey: + player.PLAYER.stopAndWait() + + def openItem(self, control=None, item=None, came_from=None): + if not item: + mli = control.getSelectedItem() + if not mli: + return + item = mli.dataSource + + self.processCommand(opener.open(item, came_from=came_from)) + + def roleClicked(self): + mli = self.rolesListControl.getSelectedItem() + if not mli: + return + + sectionRoles = busy.widthDialog(mli.dataSource.sectionRoles, '') + + if not sectionRoles: + util.DEBUG_LOG('No sections found for actor') + return + + if len(sectionRoles) > 1: + x, y = self.getRoleItemDDPosition() + + options = [{'role': r, 'display': r.reasonTitle} for r in sectionRoles] + choice = dropdown.showDropdown(options, (x, y), pos_is_bottom=True, close_direction='bottom') + + if not choice: + return + + role = choice['role'] + else: + role = sectionRoles[0] + + self.processCommand(opener.open(role)) + + def getRoleItemDDPosition(self): + y = 980 + if xbmc.getCondVisibility('Control.IsVisible(500)'): + y += 360 + if xbmc.getCondVisibility('Control.IsVisible(501)'): + y += 520 + if xbmc.getCondVisibility('Control.IsVisible(502)'): + y += 520 + if xbmc.getCondVisibility('!String.IsEmpty(Window.Property(on.extras))'): + y -= 125 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),0) + Control.IsVisible(500)'): + y -= 500 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),1) + Control.IsVisible(501)'): + y -= 500 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),1) + Control.IsVisible(502)'): + y -= 500 + + focus = int(xbmc.getInfoLabel('Container(403).Position')) + + x = ((focus + 1) * 304) - 100 + return x, y + + def getSeasons(self): + if not self.seasons: + self.seasons = self.show_.seasons() + + if not self.seasons: + return False + + return True + + def next(self): + if not self._next(): + return + self.setup() + + __next__ = next + + @busy.dialog() + def _next(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.season) + if not mli: + return False + + pos = mli.pos() + 1 + if not self.parentList.positionIsValid(pos): + pos = 0 + + self.season = self.parentList.getListItem(pos).dataSource + else: + if not self.getSeasons(): + return False + + if self.season not in self.seasons: + return False + + pos = self.seasons.index(self.season) + pos += 1 + if pos >= len(self.seasons): + pos = 0 + + self.season = self.seasons[pos] + + return True + + def prev(self): + if not self._prev(): + return + self.setup() + + @busy.dialog() + def _prev(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.season) + if not mli: + return False + + pos = mli.pos() - 1 + if pos < 0: + pos = self.parentList.size() - 1 + + self.season = self.parentList.getListItem(pos).dataSource + else: + if not self.getSeasons(): + return False + + if self.season not in self.seasons: + return False + + pos = self.seasons.index(self.season) + pos -= 1 + if pos < 0: + pos = len(self.seasons) - 1 + + self.season = self.seasons[pos] + + return True + + def searchButtonClicked(self): + section_id = self.show_.getLibrarySectionId() + self.processCommand(search.dialog(self, section_id=section_id or None)) + + def playButtonClicked(self, shuffle=False, force_episode=None): + if shuffle: + seasonOrShow = self.season or self.show_ + items = seasonOrShow.all() + pl = playlist.LocalPlaylist(items, seasonOrShow.getServer()) + + pl.shuffle(shuffle, first=True) + videoplayer.play(play_queue=pl) + return True + + else: + return self.episodeListClicked(force_episode=force_episode) + + def shuffleButtonClicked(self): + self.playButtonClicked(shuffle=True) + + def settingsButtonClicked(self): + mli = self.episodeListControl.getSelectedItem() + if not mli or mli.getProperty("is.boundary"): + return + + episode = mli.dataSource + + if not episode.mediaChoice: + playerObject = plexplayer.PlexPlayer(episode) + playerObject.build() + playersettings.showDialog(video=episode, non_playback=True) + self.setItemAudioAndSubtitleInfo(episode, mli) + + def infoButtonClicked(self): + mli = self.episodeListControl.getSelectedItem() + if not mli or mli.getProperty("is.boundary"): + return + + episode = mli.dataSource + + if episode.index: + subtitle = u'{0} {1} {2} {3}'.format(T(32303, 'Season'), episode.parentIndex, T(32304, 'Episode'), episode.index) + else: + subtitle = episode.originallyAvailableAt.asDatetime('%B %d, %Y') + + opener.handleOpen( + info.InfoWindow, + title=episode.title, + sub_title=subtitle, + thumb=episode.thumb, + thumb_fallback='script.plex/thumb_fallbacks/show.png', + info=episode.summary, + background=self.getProperty('background'), + is_16x9=True, + video=episode + ) + + def episodeListClicked(self, force_episode=None): + if not force_episode: + mli = self.episodeListControl.getSelectedItem() + if not mli or mli.getProperty("is.boundary"): + return + + episode = mli.dataSource + else: + episode = force_episode + + if not episode.available(): + util.messageDialog(T(32312, 'unavailable'), T(32332, 'This item is currently unavailable.')) + return + + resume = False + if episode.viewOffset.asInt(): + choice = dropdown.showDropdown( + options=[ + {'key': 'resume', 'display': T(32429, 'Resume from {0}').format(util.timeDisplay(episode.viewOffset.asInt()).lstrip('0').lstrip(':'))}, + {'key': 'play', 'display': T(32317, 'Play from beginning')} + ], + pos=(660, 441), + close_direction='none', + set_dropdown_prop=False, + header=T(32314, 'In Progress') + ) + + if not choice: + return + + if choice['key'] == 'resume': + resume = True + if util.advancedSettings.dialogFlickerFix: + xbmc.sleep(750) + + self._reloadVideos.append(episode) + + pl = playlist.LocalPlaylist(self.show_.all(), self.show_.getServer()) + try: + if len(pl): # Don't use playlist if it's only this video + pl.setCurrent(episode) + self.processCommand(videoplayer.play(play_queue=pl, resume=resume)) + return True + + self.processCommand(videoplayer.play(video=episode, resume=resume)) + return True + except util.NoDataException: + util.ERROR("No data - disconnected?", notify=True, time_ms=5000) + self.doClose() + + def optionsButtonClicked(self, from_item=False): + options = [] + + mli = self.episodeListControl.getSelectedItem() + + if mli and not mli.getProperty("is.boundary"): + inProgress = mli.dataSource.viewOffset.asInt() + if not mli.dataSource.isWatched or inProgress: + options.append({'key': 'mark_watched', 'display': T(32319, 'Mark Played')}) + if mli.dataSource.isWatched or inProgress: + options.append({'key': 'mark_unwatched', 'display': T(32318, 'Mark Unplayed')}) + + # if True: + # options.append({'key': 'add_to_playlist', 'display': '[COLOR FF808080]Add To Playlist[/COLOR]'}) + + if xbmc.getCondVisibility('Player.HasAudio + MusicPlayer.HasNext'): + options.append({'key': 'play_next', 'display': T(32325, 'Play Next')}) + + if self.season: + if self.season.isWatched: + options.append({'key': 'mark_season_unwatched', 'display': T(32320, 'Mark Season Unplayed')}) + else: + options.append({'key': 'mark_season_watched', 'display': T(32321, 'Mark Season Played')}) + + if self.show_: + if options: + options.append(dropdown.SEPARATOR) + + options.append({'key': 'playback_settings', 'display': T(32925, 'Playback Settings')}) + options.append(dropdown.SEPARATOR) + + if mli.dataSource.server.allowsMediaDeletion: + options.append({'key': 'delete', 'display': T(32322, 'Delete')}) + + # if xbmc.getCondVisibility('Player.HasAudio') and self.section.TYPE == 'artist': + # options.append({'key': 'add_to_queue', 'display': 'Add To Queue'}) + + if options: + options.append(dropdown.SEPARATOR) + + options.append({'key': 'to_show', 'display': T(32323, 'Go To Show')}) + options.append({'key': 'to_section', 'display': T(32324, u'Go to {0}').format( + self.show_.getLibrarySectionTitle())}) + + pos = (500, 620) + bottom = False + setDropdownProp = False + if from_item: + viewPos = self.episodeListControl.getViewPosition() + if viewPos > 6: + pos = (1490, 312 + (viewPos * 100)) + bottom = True + else: + pos = (1490, 167 + (viewPos * 100)) + bottom = False + setDropdownProp = True + + choice = dropdown.showDropdown(options, pos, pos_is_bottom=bottom, close_direction='left', + set_dropdown_prop=setDropdownProp) + if not choice: + return + + if choice['key'] == 'play_next': + xbmc.executebuiltin('PlayerControl(Next)') + elif choice['key'] == 'mark_watched': + mli.dataSource.markWatched() + self.updateItems(mli) + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'mark_unwatched': + mli.dataSource.markUnwatched() + self.updateItems(mli) + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'mark_season_watched': + self.season.markWatched() + self.updateItems() + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'mark_season_unwatched': + self.season.markUnwatched() + self.updateItems() + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'to_show': + if util.advancedSettings.dialogFlickerFix: + xbmc.sleep(750) + self.processCommand(opener.open( + self.season.parentRatingKey, + came_from=self.season.parentRatingKey) + ) + elif choice['key'] == 'to_section': + self.goHome(self.show_.getLibrarySectionId()) + elif choice['key'] == 'delete': + self.delete() + elif choice['key'] == 'playback_settings': + self.playbackSettings(self.show_, pos, bottom) + + def mediaButtonClicked(self): + options = [] + mli = self.episodeListControl.getSelectedItem() + ds = mli.dataSource + for media in ds.media: + ind = '' + if ds.mediaChoice and media.id == ds.mediaChoice.media.id: + ind = 'script.plex/home/device/check.png' + options.append({'key': media, 'display': media.versionString(), 'indicator': ind}) + choice = dropdown.showDropdown(options, header=T(32450, 'Choose Version'), with_indicator=True) + if not choice: + return False + + for media in ds.media: + media.set('selected', '') + + ds.setMediaChoice(choice['key']) + choice['key'].set('selected', 1) + self.setPostReloadItemInfo(ds, mli) + + def delete(self): + button = optionsdialog.show( + T(32326, 'Really delete?'), + T(32327, 'Are you sure you really want to delete this media?'), + T(32328, 'Yes'), + T(32329, 'No') + ) + + if button != 0: + return + + if not self._delete(): + util.messageDialog(T(32330, 'Message'), T(32331, 'There was a problem while attempting to delete the media.')) + + @busy.dialog() + def _delete(self): + mli = self.episodeListControl.getSelectedItem() + if not mli or mli.getProperty("is.boundary"): + return + + video = mli.dataSource + success = video.delete() + util.LOG('Media DELETE: {0} - {1}'.format(video, success and 'SUCCESS' or 'FAILED')) + if success: + self.episodeListControl.removeItem(mli.pos()) + if not self.episodeListControl.size(): + self.doClose() + else: + (self.season or self.show_).reload() + return success + + def checkForHeaderFocus(self, action): + # don't continue if we're still waiting for tasks + if self.tasks or not self.episodesPaginator: + if self.tasks: + util.DEBUG_LOG("Episodes: Moving too fast through paginator, throttling.") + return + + if self.episodesPaginator.boundaryHit: + items = self.episodesPaginator.paginate() + self.reloadItems(items) + return True + + mli = self.episodeListControl.getSelectedItem() + if not mli or mli.getProperty("is.boundary"): + return + + lastItem = self.lastItem + + if action in (xbmcgui.ACTION_MOVE_RIGHT, xbmcgui.ACTION_MOVE_LEFT) and lastItem: + items = self.episodesPaginator.wrap(mli, lastItem, action) + xbmc.sleep(100) + mli = self.episodeListControl.getSelectedItem() + if items: + self.reloadItems(items) + return True + + if mli != self.lastItem and not mli.getProperty("is.boundary"): + self.lastItem = mli + self.setProgress(mli) + + if action in (xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_PAGE_UP): + if mli.getProperty('is.header'): + xbmc.executebuiltin('Action(up)') + if action in (xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_PAGE_DOWN, xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT): + if mli.getProperty('is.header'): + xbmc.executebuiltin('Action(down)') + + def updateProperties(self): + showTitle = self.show_ and self.show_.title or '' + + self.updateBackgroundFrom(self.show_ or self.season.show()) + self.setProperty('season.thumb', (self.season or self.show_).thumb.asTranscodedImageURL(*self.POSTER_DIM)) + self.setProperty('show.title', showTitle) + self.setProperty('season.title', (self.season or self.show_).title) + + if self.season: + self.setProperty('episodes.header', u'{0} \u2022 {1} {2}'.format(showTitle, T(32303, 'Season'), self.season.index)) + self.setProperty('extras.header', u'{0} \u2022 {1} {2}'.format(T(32305, 'Extras'), T(32303, 'Season'), self.season.index)) + else: + self.setProperty('episodes.header', u'Episodes') + self.setProperty('extras.header', u'Extras') + + self.setProperty('seasons.header', + u'{0} \u2022 {1}'.format(showTitle, T(32942, 'Seasons'))) + self.setProperty('related.header', T(32306, 'Related Shows')) + self.genre = self.show_.genres() and self.show_.genres()[0].tag or '' + + @busy.dialog() + def updateItems(self, item=None): + if item: + item.setProperty('unwatched', not item.dataSource.isWatched and '1' or '') + self.setProgress(item) + item.setProperty('progress', util.getProgressImage(item.dataSource)) + (self.season or self.show_).reload() + else: + self.fillEpisodes(update=True) + + if self.episode: + self.episode.reload() + + def setItemInfo(self, video, mli): + # video.reload(checkFiles=1) + mli.setProperty('background', util.backgroundFromArt(video.art, width=self.width, height=self.height)) + mli.setProperty('title', video.title) + mli.setProperty('show.title', video.grandparentTitle or (self.show_.title if self.show_ else '')) + mli.setProperty('duration', util.durationToText(video.duration.asInt())) + mli.setProperty('summary', video.summary.strip().replace('\t', ' ')) + mli.setProperty('video.rendering', video.videoCodecRendering) + + if video.index: + mli.setProperty('season', u'{0} {1}'.format(T(32303, 'Season'), video.parentIndex)) + mli.setProperty('episode', u'{0} {1}'.format(T(32304, 'Episode'), video.index)) + else: + mli.setProperty('season', '') + mli.setProperty('episode', '') + + mli.setProperty('date', util.cleanLeadingZeros(video.originallyAvailableAt.asDatetime('%B %d, %Y'))) + + # mli.setProperty('related.header', 'Related Shows') + mli.setProperty('year', video.year) + mli.setProperty('content.rating', video.contentRating.split('/', 1)[-1]) + mli.setProperty('genre', self.genre) + + if video.get('userRating'): + stars = str(int(round((video.userRating.asFloat() / 10) * 5))) + mli.setProperty('rating.stars', stars) + # elif video.rating: + # stars = str(int(round((video.rating.asFloat() / 10) * 5))) + # mli.setProperty('rating.stars', stars) + + if video.get('ratingImage'): + rating = video.rating + audienceRating = video.audienceRating + if video.ratingImage.startswith('rottentomatoes:'): + rating = '{0}%'.format(int(rating.asFloat() * 10)) + if audienceRating: + audienceRating = '{0}%'.format(int(audienceRating.asFloat() * 10)) + + mli.setProperty('rating', rating) + mli.setProperty('rating.image', 'script.plex/ratings/{0}.png'.format(video.ratingImage.replace('://', '/'))) + if video.get('audienceRatingImage'): + mli.setProperty('rating2', audienceRating) + mli.setProperty('rating2.image', 'script.plex/ratings/{0}.png'.format(video.audienceRatingImage.replace('://', '/'))) + else: + mli.setProperty('rating', video.rating) + + def setPostReloadItemInfo(self, video, mli): + self.setItemAudioAndSubtitleInfo(video, mli) + mli.setProperty('unwatched', not video.isWatched and '1' or '') + mli.setProperty('video.res', video.resolutionString()) + mli.setProperty('audio.codec', video.audioCodecString()) + mli.setProperty('video.codec', video.videoCodecString()) + mli.setProperty('audio.channels', video.audioChannelsString(metadata.apiTranslate)) + mli.setProperty('video.rendering', video.videoCodecRendering) + mli.setBoolProperty('unavailable', not video.available()) + + defW = 176 + defH = 140 + ids = [301, 302, 303, 304, 305] + if len(list(filter(lambda x: x.isAccessible(), video.media()))) > 1: + mli.setBoolProperty('media.multiple', True) + # adjust button sizes + ids.append(307) + for id in ids: + ctrl = self.getControl(id) + ctrl.setWidth(161) + ctrl.setHeight(125) + del ctrl + else: + mli.setBoolProperty('media.multiple', False) + # reset button sizes + for id in ids: + ctrl = self.getControl(id) + ctrl.setWidth(defW) + ctrl.setHeight(defH) + del ctrl + + def setItemAudioAndSubtitleInfo(self, video, mli): + sas = video.selectedAudioStream() + mli.setProperty('audio', sas and sas.getTitle(metadata.apiTranslate) or T(32309, 'None')) + + sss = video.selectedSubtitleStream(forced_subtitles_override= + util.getSetting("forced_subtitles_override", False)) + if sss: + if len(video.subtitleStreams) > 1: + mli.setProperty( + 'subtitles', u'{0} \u2022 {1} {2}'.format(sss.getTitle(metadata.apiTranslate), len(video.subtitleStreams) - 1, T(32307, 'More')) + ) + else: + mli.setProperty('subtitles', sss.getTitle(metadata.apiTranslate)) + else: + if video.subtitleStreams: + mli.setProperty('subtitles', u'{0} \u2022 {1} {2}'.format(T(32309, 'None'), len(video.subtitleStreams), T(32308, 'Available'))) + else: + mli.setProperty('subtitles', T(32309, 'None')) + + def setProgress(self, mli): + video = mli.dataSource + if video.viewOffset.asInt(): + width = video.viewOffset.asInt() and (1 + int((video.viewOffset.asInt() / video.duration.asFloat()) * self.width)) or 1 + self.progressImageControl.setWidth(width) + else: + self.progressImageControl.setWidth(1) + + if video.viewOffset.asInt(): + mli.setProperty('remainingTime', T(33615, "{time} left").format(time=video.remainingTimeString)) + else: + mli.setProperty('remainingTime', '') + + def createListItem(self, episode): + if episode.index: + subtitle = u'{0}{1} \u2022 {2}{3}'.format(T(32310, 'S'), episode.parentIndex, T(32311, 'E'), episode.index) + else: + subtitle = episode.originallyAvailableAt.asDatetime('%m/%d/%y') + + mli = kodigui.ManagedListItem( + episode.title, + subtitle, + thumbnailImage=episode.thumb.asTranscodedImageURL(*self.THUMB_AR16X9_DIM), + data_source=episode + ) + mli.setProperty('episode.number', str(episode.index) or '') + mli.setProperty('episode.duration', util.durationToText(episode.duration.asInt())) + mli.setProperty('unwatched', not episode.isWatched and '1' or '') + # mli.setProperty('progress', util.getProgressImage(obj)) + return mli + + def fillEpisodes(self, update=False): + items = self.episodesPaginator.paginate() + self.reloadItems(items) + + def reloadItems(self, items, with_progress=False): + tasks = [] + for mli in items: + if not mli.dataSource: + continue + + task = EpisodeReloadTask().setup(mli.dataSource, self.reloadItemCallback, with_progress=with_progress) + self.tasks.add(task) + tasks.append(task) + + backgroundthread.BGThreader.addTasks(tasks) + + def reloadItemCallback(self, task, episode, with_progress=False): + self.tasks.remove(task) + del task + + if self.closing: + return + + selected = self.episodeListControl.getSelectedItem() + + for mli in self.episodeListControl: + if mli.dataSource == episode: + if not episode.mediaChoice: + episode.setMediaChoice() + + self.setPostReloadItemInfo(episode, mli) + if with_progress: + self.episodesPaginator.prepareListItem(None, mli) + if mli == selected: + self.lastItem = mli + self.setProgress(mli) + return + + def fillExtras(self, has_prev=False): + items = [] + idx = 0 + + seasonOrShow = self.season or self.show_ + + if not seasonOrShow.extras: + self.extraListControl.reset() + return False + + for extra in seasonOrShow.extras(): + mli = kodigui.ManagedListItem( + extra.title or '', + metadata.EXTRA_MAP.get(extra.extraType.asInt(), ''), + thumbnailImage=extra.thumb.asTranscodedImageURL(*self.EXTRA_DIM), + data_source=extra + ) + + if mli: + mli.setProperty('index', str(idx)) + mli.setProperty( + 'thumb.fallback', 'script.plex/thumb_fallbacks/{0}.png'.format(extra.type in ('show', 'season', 'episode') and 'show' or 'movie') + ) + items.append(mli) + idx += 1 + + if not items: + return False + + self.extraListControl.reset() + self.extraListControl.addItems(items) + + self.setProperty('divider.{0}'.format(self.EXTRA_LIST_ID), has_prev and '1' or '') + return True + + def fillRelated(self, has_prev=False): + if not self.relatedPaginator.leafCount: + self.relatedListControl.reset() + return has_prev + + items = self.relatedPaginator.paginate() + if not items: + return False + + self.setProperty('divider.{0}'.format(self.RELATED_LIST_ID), has_prev and '1' or '') + + return True + + def fillRoles(self, has_prev=False): + items = [] + idx = 0 + + if not self.show_.roles: + self.rolesListControl.reset() + return False + + for role in self.show_.roles(): + mli = kodigui.ManagedListItem(role.tag, role.role, thumbnailImage=role.thumb.asTranscodedImageURL(*self.ROLES_DIM), data_source=role) + mli.setProperty('index', str(idx)) + items.append(mli) + idx += 1 + + if not items: + return False + + self.setProperty('divider.{0}'.format(self.ROLES_LIST_ID), has_prev and '1' or '') + + self.rolesListControl.reset() + self.rolesListControl.addItems(items) + return True diff --git a/script.plexmod/lib/windows/home.py b/script.plexmod/lib/windows/home.py new file mode 100644 index 000000000..0e0584cbb --- /dev/null +++ b/script.plexmod/lib/windows/home.py @@ -0,0 +1,1388 @@ +from __future__ import absolute_import +import time +import threading +import re + +from kodi_six import xbmc +from kodi_six import xbmcgui + +from . import kodigui +from lib import util +from lib import backgroundthread +from lib import colors +from lib import player + +import plexnet +from plexnet import plexapp + +from . import windowutils +from . import playlists +from . import busy +from . import opener +from . import search +from . import optionsdialog + +from lib.util import T +from six.moves import range + + +HUBS_REFRESH_INTERVAL = 300 # 5 Minutes +HUB_PAGE_SIZE = 10 + +MOVE_SET = frozenset( + ( + xbmcgui.ACTION_MOVE_LEFT, + xbmcgui.ACTION_MOVE_RIGHT, + xbmcgui.ACTION_MOVE_UP, + xbmcgui.ACTION_MOVE_DOWN, + xbmcgui.ACTION_MOUSE_MOVE, + xbmcgui.ACTION_PAGE_UP, + xbmcgui.ACTION_PAGE_DOWN, + xbmcgui.ACTION_FIRST_PAGE, + xbmcgui.ACTION_LAST_PAGE, + xbmcgui.ACTION_MOUSE_WHEEL_DOWN, + xbmcgui.ACTION_MOUSE_WHEEL_UP + ) +) + + +class HubsList(list): + def init(self): + self.lastUpdated = time.time() + return self + + +class SectionHubsTask(backgroundthread.Task): + def setup(self, section, callback): + self.section = section + self.callback = callback + return self + + def run(self): + if self.isCanceled(): + return + + if not plexapp.SERVERMANAGER.selectedServer: + # Could happen during sign-out for instance + return + + try: + hubs = HubsList(plexapp.SERVERMANAGER.selectedServer.hubs(self.section.key, count=HUB_PAGE_SIZE)).init() + if self.isCanceled(): + return + self.callback(self.section, hubs) + except plexnet.exceptions.BadRequest: + util.DEBUG_LOG('404 on section: {0}'.format(repr(self.section.title))) + self.callback(self.section, False) + except TypeError: + util.ERROR("No data - disconnected?", notify=True, time_ms=5000) + self.cancel() + + +class UpdateHubTask(backgroundthread.Task): + def setup(self, hub, callback): + self.hub = hub + self.callback = callback + return self + + def run(self): + if self.isCanceled(): + return + + if not plexapp.SERVERMANAGER.selectedServer: + # Could happen during sign-out for instance + return + + try: + self.hub.reload(limit=HUB_PAGE_SIZE) + if self.isCanceled(): + return + self.callback(self.hub) + except plexnet.exceptions.BadRequest: + util.DEBUG_LOG('404 on section: {0}'.format(repr(self.section.title))) + + +class ExtendHubTask(backgroundthread.Task): + def setup(self, hub, callback, canceledCallback=None): + self.hub = hub + self.callback = callback + self.canceledCallback = canceledCallback + return self + + def run(self): + if self.isCanceled(): + if self.canceledCallback: + self.canceledCallback(self.hub) + return + + if not plexapp.SERVERMANAGER.selectedServer: + # Could happen during sign-out for instance + return + + try: + start = self.hub.offset.asInt() + self.hub.size.asInt() + items = self.hub.extend(start=start, size=HUB_PAGE_SIZE) + if self.isCanceled(): + if self.canceledCallback: + self.canceledCallback(self.hub) + return + self.callback(self.hub, items) + except plexnet.exceptions.BadRequest: + util.DEBUG_LOG('404 on hub: {0}'.format(repr(self.hub.hubIdentifier))) + if self.canceledCallback: + self.canceledCallback(self.hub) + + +class HomeSection(object): + key = None + type = 'home' + title = T(32332, 'Home') + + +class PlaylistsSection(object): + key = 'playlists' + type = 'playlists' + title = T(32333, 'Playlists') + + +class ServerListItem(kodigui.ManagedListItem): + uuid = None + + def hookSignals(self): + self.dataSource.on('completed:reachability', self.onReachability) + self.dataSource.on('started:reachability', self.onReachability) + + def unHookSignals(self): + try: + self.dataSource.off('completed:reachability', self.onReachability) + self.dataSource.off('started:reachability', self.onReachability) + except: + pass + + def setRefreshing(self): + self.safeSetProperty('status', 'refreshing.gif') + + def safeSetProperty(self, key, value): + # For if we catch the item in the middle of being removed + try: + self.setProperty(key, value) + return True + except AttributeError: + pass + + return False + + def safeSetLabel(self, value, func="setLabel"): + if value is None: + return False + try: + getattr(self, func)(value) + return True + except AttributeError: + pass + + return False + + def safeGetDSProperty(self, prop): + return getattr(self.dataSource, prop, None) + + def onReachability(self, **kwargs): + plexapp.util.APP.trigger('sli:reachability:received') + return self.onUpdate(**kwargs) + + def onUpdate(self, **kwargs): + if not self.listItem: # ex. can happen on Kodi shutdown + return + + if self.dataSource == kodigui.DUMMY_DATA_SOURCE: + return + + # this looks a little ridiculous, but we're experiencing timing issues here + isSupported = self.safeGetDSProperty("isSupported") + isReachable = False + isReachableFunc = self.safeGetDSProperty("isReachable") + isSecure = self.safeGetDSProperty("isSecure") + isLocal = self.safeGetDSProperty("isLocal") + name = self.safeGetDSProperty("name") + pendingReachabilityRequests = self.safeGetDSProperty("pendingReachabilityRequests") + owned = not self.safeGetDSProperty("owned") and self.safeGetDSProperty("owner") or '' + if isReachableFunc: + isReachable = isReachableFunc() + + if not isSupported or not isReachable: + if pendingReachabilityRequests is not None and pendingReachabilityRequests > 0: + self.safeSetProperty('status', 'refreshing.gif') + else: + self.safeSetProperty('status', 'unreachable.png') + else: + self.safeSetProperty('status', isSecure and 'secure.png' or '') + self.safeSetProperty('secure', isSecure and '1' or '') + self.safeSetProperty('local', isLocal and '1' or '') + + self.safeSetProperty('current', plexapp.SERVERMANAGER.selectedServer.uuid == self.uuid and '1' or '') + if name: + self.safeSetLabel(name) + + if owned: + self.safeSetLabel(owned, func="setLabel2") + + def onDestroy(self): + self.unHookSignals() + + +class HomeWindow(kodigui.BaseWindow, util.CronReceiver): + xmlFile = 'script-plex-home.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + OPTIONS_GROUP_ID = 200 + + SECTION_LIST_ID = 101 + SERVER_BUTTON_ID = 201 + + USER_BUTTON_ID = 202 + USER_LIST_ID = 250 + + SEARCH_BUTTON_ID = 203 + SERVER_LIST_ID = 260 + REFRESH_SL_ID = 262 + + PLAYER_STATUS_BUTTON_ID = 204 + + HUB_AR16X9_00 = 400 + HUB_POSTER_01 = 401 + HUB_POSTER_02 = 402 + HUB_POSTER_03 = 403 + HUB_POSTER_04 = 404 + HUB_SQUARE_05 = 405 + HUB_AR16X9_06 = 406 + HUB_POSTER_07 = 407 + HUB_POSTER_08 = 408 + HUB_SQUARE_09 = 409 + HUB_SQUARE_10 = 410 + HUB_SQUARE_11 = 411 + HUB_SQUARE_12 = 412 + HUB_POSTER_13 = 413 + HUB_POSTER_14 = 414 + HUB_POSTER_15 = 415 + HUB_POSTER_16 = 416 + HUB_AR16X9_17 = 417 + HUB_AR16X9_18 = 418 + HUB_AR16X9_19 = 419 + + HUB_SQUARE_20 = 420 + HUB_SQUARE_21 = 421 + HUB_SQUARE_22 = 422 + + HUB_AR16X9_23 = 423 + + HUBMAP = { + # HOME + 'home.continue': {'index': 0, 'with_progress': True, 'with_art': True, 'do_updates': True, 'text2lines': True}, + 'home.ondeck': {'index': 1, 'with_progress': True, 'do_updates': True, 'text2lines': True}, + 'home.television.recent': {'index': 2, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + 'home.movies.recent': {'index': 4, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + 'home.music.recent': {'index': 5, 'text2lines': True}, + 'home.videos.recent': {'index': 6, 'with_progress': True, 'ar16x9': True}, + #'home.playlists': {'index': 9}, # No other Plex home screen shows playlists so removing it from here + 'home.photos.recent': {'index': 10, 'text2lines': True}, + # SHOW + 'tv.ondeck': {'index': 1, 'with_progress': True, 'do_updates': True, 'text2lines': True}, + 'tv.recentlyaired': {'index': 2, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + 'tv.recentlyadded': {'index': 3, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + 'tv.inprogress': {'index': 4, 'with_progress': True, 'do_updates': True, 'text2lines': True}, + 'tv.startwatching': {'index': 7, 'with_progress': True, 'do_updates': True}, + 'tv.rediscover': {'index': 8, 'with_progress': True, 'do_updates': True}, + 'tv.morefromnetwork': {'index': 13, 'with_progress': True, 'do_updates': True}, + 'tv.toprated': {'index': 14, 'with_progress': True, 'do_updates': True}, + 'tv.moreingenre': {'index': 15, 'with_progress': True, 'do_updates': True}, + 'tv.recentlyviewed': {'index': 16, 'with_progress': True, 'text2lines': True, 'do_updates': True}, + # MOVIE + 'movie.inprogress': {'index': 0, 'with_progress': True, 'with_art': True, 'do_updates': True, 'text2lines': True}, + 'movie.recentlyreleased': {'index': 1, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + 'movie.recentlyadded': {'index': 2, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + 'movie.genre': {'index': 3, 'with_progress': True, 'text2lines': True, 'do_updates': True}, + 'movie.by.actor.or.director': {'index': 7, 'with_progress': True, 'text2lines': True, 'do_updates': True}, + 'movie.topunwatched': {'index': 13, 'text2lines': True, 'do_updates': True}, + 'movie.recentlyviewed': {'index': 14, 'with_progress': True, 'text2lines': True, 'do_updates': True}, + # ARTIST + 'music.recent.played': {'index': 5, 'do_updates': True}, + 'music.recent.added': {'index': 9, 'text2lines': True}, + 'music.recent.artist': {'index': 10, 'text2lines': True}, + 'music.recent.genre': {'index': 11, 'text2lines': True}, + 'music.top.period': {'index': 12, 'text2lines': True}, + 'music.popular': {'index': 20, 'text2lines': True}, + 'music.recent.label': {'index': 21, 'text2lines': True}, + 'music.touring': {'index': 22}, + 'music.videos.popular.new': {'index': 18}, + 'music.videos.new': {'index': 19}, + 'music.videos.recent.artists': {'index': 23}, + # PHOTO + 'photo.recent': {'index': 5, 'text2lines': True}, + 'photo.random.year': {'index': 9, 'text2lines': True}, + 'photo.random.decade': {'index': 10, 'text2lines': True}, + 'photo.random.dayormonth': {'index': 11, 'text2lines': True}, + # VIDEO + 'video.recent': {'index': 0, 'with_progress': True, 'ar16x9': True}, + 'video.random.year': {'index': 6, 'with_progress': True, 'ar16x9': True}, + 'video.random.decade': {'index': 17, 'with_progress': True, 'ar16x9': True}, + 'video.inprogress': {'index': 18, 'with_progress': True, 'ar16x9': True}, + 'video.unwatched.random': {'index': 19, 'ar16x9': True}, + 'video.recentlyviewed': {'index': 23, 'with_progress': True, 'ar16x9': True}, + # PLAYLISTS + 'playlists.audio': {'index': 5, 'text2lines': True, 'title': T(32048, 'Audio')}, + 'playlists.video': {'index': 6, 'text2lines': True, 'ar16x9': True, 'title': T(32053, 'Video')}, + } + + THUMB_POSTER_DIM = util.scaleResolution(244, 361) + THUMB_AR16X9_DIM = util.scaleResolution(532, 299) + THUMB_SQUARE_DIM = util.scaleResolution(244, 244) + + def __init__(self, *args, **kwargs): + kodigui.BaseWindow.__init__(self, *args, **kwargs) + self.lastSection = HomeSection + self.tasks = [] + self.closeOption = None + self.hubControls = None + self.backgroundSet = False + self.sectionChangeThread = None + self.sectionChangeTimeout = 0 + self.lastFocusID = None + self.lastNonOptionsFocusID = None + self.sectionHubs = {} + self.updateHubs = {} + self.changingServer = False + self._shuttingDown = False + windowutils.HOME = self + + self.lock = threading.Lock() + + util.setGlobalBoolProperty('off.sections', '') + + def onFirstInit(self): + # set last BG image if possible + if util.advancedSettings.dynamicBackgrounds: + bgUrl = util.getSetting("last_bg_url") + if bgUrl: + self.windowSetBackground(bgUrl) + + # set good volume if we've missed re-setting BGM volume before + lastGoodVlm = util.getSetting('last_good_volume', 0) + BGMVlm = plexapp.util.INTERFACE.getThemeMusicValue() + if lastGoodVlm and BGMVlm and util.rpc.Application.GetProperties(properties=["volume"])["volume"] == BGMVlm: + util.DEBUG_LOG("Setting volume to {}, we probably missed the " + "re-set on the last BGM encounter".format(lastGoodVlm)) + xbmc.executebuiltin("SetVolume({})".format(lastGoodVlm)) + + self.sectionList = kodigui.ManagedControlList(self, self.SECTION_LIST_ID, 7) + self.serverList = kodigui.ManagedControlList(self, self.SERVER_LIST_ID, 10) + self.userList = kodigui.ManagedControlList(self, self.USER_LIST_ID, 3) + + self.hubControls = ( + kodigui.ManagedControlList(self, self.HUB_AR16X9_00, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_01, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_02, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_03, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_04, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_05, 5), + kodigui.ManagedControlList(self, self.HUB_AR16X9_06, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_07, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_08, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_09, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_10, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_11, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_12, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_13, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_14, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_15, 5), + kodigui.ManagedControlList(self, self.HUB_POSTER_16, 5), + kodigui.ManagedControlList(self, self.HUB_AR16X9_17, 5), + kodigui.ManagedControlList(self, self.HUB_AR16X9_18, 5), + kodigui.ManagedControlList(self, self.HUB_AR16X9_19, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_20, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_21, 5), + kodigui.ManagedControlList(self, self.HUB_SQUARE_22, 5), + kodigui.ManagedControlList(self, self.HUB_AR16X9_23, 5), + ) + + self.hubFocusIndexes = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 17, 18, 19, 20, 21, 22, 13, 14, 15, 23) + + self.bottomItem = 0 + if self.serverRefresh(): + self.setFocusId(self.SECTION_LIST_ID) + + self.hookSignals() + util.CRON.registerReceiver(self) + + def onReInit(self): + if self.lastFocusID: + # try focusing the last focused ID. if that's a hub and it's empty (=not focusable), try focusing the + # next best hub + if 399 < self.lastFocusID < 500: + hubControlIndex = self.lastFocusID - 400 + + if hubControlIndex in self.hubFocusIndexes and self.hubControls[hubControlIndex]: + # this is basically just used for setting the background upon reinit + # fixme: declutter, separation of concerns + self.checkHubItem(self.lastFocusID) + else: + util.DEBUG_LOG("Focus requested on {}, which can't focus. Trying next hub".format(self.lastFocusID)) + self.focusFirstValidHub(hubControlIndex) + + else: + self.setFocusId(self.lastFocusID) + + def focusFirstValidHub(self, startIndex=None): + indices = self.hubFocusIndexes + if startIndex is not None: + try: + indices = self.hubFocusIndexes[self.hubFocusIndexes.index(startIndex):] + util.DEBUG_LOG("Trying to focus the next best hub after: %i" % (400 + startIndex)) + except IndexError: + pass + + for index in indices: + if self.hubControls[index]: + util.DEBUG_LOG("Focusing hub: %i" % (400 + index)) + self.setFocusId(400+index) + self.checkHubItem(400+index) + return + + if startIndex is not None: + util.DEBUG_LOG("Tried all possible hubs after %i. Continuing from the top" % (400 + startIndex)) + else: + util.DEBUG_LOG("Can't find any suitable hub to focus. This is bad.") + self.setFocusId(self.SECTION_LIST_ID) + return + + return self.focusFirstValidHub() + + def hookSignals(self): + plexapp.SERVERMANAGER.on('new:server', self.onNewServer) + plexapp.SERVERMANAGER.on('remove:server', self.onRemoveServer) + plexapp.SERVERMANAGER.on('reachable:server', self.onReachableServer) + plexapp.SERVERMANAGER.on('reachable:server', self.displayServerAndUser) + + plexapp.util.APP.on('change:selectedServer', self.onSelectedServerChange) + plexapp.util.APP.on('account:response', self.displayServerAndUser) + plexapp.util.APP.on('sli:reachability:received', self.displayServerAndUser) + + player.PLAYER.on('session.ended', self.updateOnDeckHubs) + util.MONITOR.on('changed.watchstatus', self.updateOnDeckHubs) + + def unhookSignals(self): + plexapp.SERVERMANAGER.off('new:server', self.onNewServer) + plexapp.SERVERMANAGER.off('remove:server', self.onRemoveServer) + plexapp.SERVERMANAGER.off('reachable:server', self.onReachableServer) + plexapp.SERVERMANAGER.off('reachable:server', self.displayServerAndUser) + + plexapp.util.APP.off('change:selectedServer', self.onSelectedServerChange) + plexapp.util.APP.off('account:response', self.displayServerAndUser) + + player.PLAYER.off('session.ended', self.updateOnDeckHubs) + util.MONITOR.off('changed.watchstatus', self.updateOnDeckHubs) + + def tick(self): + if not self.lastSection: + return + + hubs = self.sectionHubs.get(self.lastSection.key) + if not hubs: + return + + if time.time() - hubs.lastUpdated > HUBS_REFRESH_INTERVAL: + self.showHubs(self.lastSection, update=True) + + def shutdown(self): + self._shuttingDown = True + try: + self.serverList.reset() + except AttributeError: + pass + + self.unhookSignals() + + if util.advancedSettings.dynamicBackgrounds: + # store BG url of first hub, first item, as this is most likely to be the one we're focusing on the + # next start + try: + indices = self.hubFocusIndexes + for index in indices: + if self.hubControls[index]: + ds = self.hubControls[index][0].dataSource + util.setSetting("last_bg_url", + util.backgroundFromArt(ds.art, width=self.width, height=self.height)) + return + except: + util.LOG("Couldn't store last background") + + def onAction(self, action): + controlID = self.getFocusId() + + try: + if not controlID and not action == xbmcgui.ACTION_MOUSE_MOVE: + if self.lastFocusID: + self.setFocusId(self.lastFocusID) + + if controlID == self.SECTION_LIST_ID: + self.checkSectionItem(action=action) + + if controlID == self.SERVER_BUTTON_ID: + if action == xbmcgui.ACTION_SELECT_ITEM: + self.showServers() + return + elif action == xbmcgui.ACTION_MOUSE_LEFT_CLICK: + self.showServers(mouse=True) + self.setBoolProperty('show.servers', True) + return + elif controlID == self.USER_BUTTON_ID: + if action == xbmcgui.ACTION_SELECT_ITEM: + self.showUserMenu() + return + elif action == xbmcgui.ACTION_MOUSE_LEFT_CLICK: + self.showUserMenu(mouse=True) + self.setBoolProperty('show.options', True) + return + elif controlID == self.SERVER_LIST_ID: + if action == xbmcgui.ACTION_SELECT_ITEM: + self.setFocusId(self.SERVER_BUTTON_ID) + return + + if controlID == self.SERVER_BUTTON_ID and action == xbmcgui.ACTION_MOVE_RIGHT: + self.setFocusId(self.USER_BUTTON_ID) + elif controlID == self.USER_BUTTON_ID and action == xbmcgui.ACTION_MOVE_LEFT: + self.setFocusId(self.SERVER_BUTTON_ID) + elif controlID == self.SEARCH_BUTTON_ID and action == xbmcgui.ACTION_MOVE_RIGHT: + if xbmc.getCondVisibility('Player.HasMedia + Control.IsVisible({0})'.format(self.PLAYER_STATUS_BUTTON_ID)): + self.setFocusId(self.PLAYER_STATUS_BUTTON_ID) + else: + self.setFocusId(self.SERVER_BUTTON_ID) + elif controlID == self.PLAYER_STATUS_BUTTON_ID and action == xbmcgui.ACTION_MOVE_RIGHT: + self.setFocusId(self.SERVER_BUTTON_ID) + elif 399 < controlID < 500: + if action.getId() in MOVE_SET: + self.checkHubItem(controlID) + elif action.getId() == xbmcgui.ACTION_PLAYER_PLAY: + self.hubItemClicked(controlID, auto_play=True) + return + + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_CONTEXT_MENU): + optionsFocused = xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)) + offSections = util.getGlobalProperty('off.sections') + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU): + # fixme: cheap way of avoiding an early exit after a server change + if self.changingServer: + return + + if self.getFocusId() == self.USER_LIST_ID: + self.setFocusId(self.USER_BUTTON_ID) + return + elif self.getFocusId() == self.SERVER_LIST_ID: + self.setFocusId(self.SERVER_BUTTON_ID) + return + + if util.advancedSettings.fastBack and not optionsFocused and offSections \ + and self.lastFocusID not in (self.USER_BUTTON_ID, self.SERVER_BUTTON_ID, + self.SEARCH_BUTTON_ID, self.SECTION_LIST_ID): + self.setProperty('hub.focus', '0') + self.setFocusId(self.SECTION_LIST_ID) + return + + if action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + if not optionsFocused and offSections \ + and (not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): + self.lastNonOptionsFocusID = self.lastFocusID + self.setFocusId(self.OPTIONS_GROUP_ID) + return + elif action == xbmcgui.ACTION_CONTEXT_MENU and optionsFocused and offSections \ + and self.lastNonOptionsFocusID: + self.setFocusId(self.lastNonOptionsFocusID) + self.lastNonOptionsFocusID = None + return + + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU): + ex = self.confirmExit() + # 0 = exit; 1 = minimize; 2 = cancel + if ex in (2, None): + return + elif ex == 1: + xbmc.executebuiltin('ActivateWindow(10000)') + return + elif ex == 0: + self._shuttingDown = True + # 0 passes the action to the BaseWindow and exits HOME + except: + util.ERROR() + + kodigui.BaseWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.SECTION_LIST_ID: + self.sectionClicked() + # elif controlID == self.SERVER_BUTTON_ID: + # self.showServers() + elif controlID == self.SERVER_LIST_ID: + self.setBoolProperty('show.servers', False) + self.selectServer() + # elif controlID == self.USER_BUTTON_ID: + # self.showUserMenu() + elif controlID == self.USER_LIST_ID: + self.doUserOption() + self.setBoolProperty('show.options', False) + self.setFocusId(self.USER_BUTTON_ID) + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif 399 < controlID < 500: + self.hubItemClicked(controlID) + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def onFocus(self, controlID): + if controlID != 204 and controlID < 500: + # don't store focus for mini music player + self.lastFocusID = controlID + + if 399 < controlID < 500: + self.setProperty('hub.focus', str(self.hubFocusIndexes[controlID - 400])) + + if controlID == self.SECTION_LIST_ID and not self.changingServer: + self.checkSectionItem() + + if xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + ControlGroup(100).HasFocus(0)'): + util.setGlobalBoolProperty('off.sections', '') + elif controlID != 250 and xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + !ControlGroup(100).HasFocus(0)'): + util.setGlobalBoolProperty('off.sections', '1') + + if player.PLAYER.bgmPlaying: + player.PLAYER.stopAndWait() + + def confirmExit(self): + button = optionsdialog.show( + T(32334, 'Confirm Exit'), + T(32335, 'Are you ready to exit Plex?'), + T(32336, 'Exit'), + T(32924, 'Minimize'), + T(32337, 'Cancel') + ) + + return button + + def searchButtonClicked(self): + self.processCommand(search.dialog(self)) + + def updateOnDeckHubs(self, **kwargs): + if util.getSetting("speedy_home_hubs2", False): + util.DEBUG_LOG("Using alternative home hub refresh") + sections = set() + for mli in self.sectionList: + if mli.dataSource is not None and mli.dataSource != self.lastSection: + sections.add(mli.dataSource) + tasks = [SectionHubsTask().setup(s, self.sectionHubsCallback) for s in [self.lastSection] + list(sections)] + else: + tasks = [UpdateHubTask().setup(hub, self.updateHubCallback) for hub in self.updateHubs.values()] + self.tasks += tasks + backgroundthread.BGThreader.addTasks(tasks) + + def showBusy(self, on=True): + self.setProperty('busy', on and '1' or '') + + @busy.dialog() + def serverRefresh(self): + backgroundthread.BGThreader.reset() + if self.tasks: + for task in self.tasks: + task.cancel() + + with self.lock: + self.setProperty('hub.focus', '') + self.displayServerAndUser() + if not plexapp.SERVERMANAGER.selectedServer: + self.setFocusId(self.USER_BUTTON_ID) + return False + + self.showSections() + self.backgroundSet = False + self.showHubs(HomeSection) + return True + + def hubItemClicked(self, hubControlID, auto_play=False): + control = self.hubControls[hubControlID - 400] + mli = control.getSelectedItem() + if not mli: + return + + if mli.dataSource is None: + return + + try: + command = opener.open(mli.dataSource, auto_play=auto_play) + if command == "NODATA": + raise util.NoDataException + except util.NoDataException: + util.ERROR("No data - disconnected?", notify=True, time_ms=5000) + return + + self.updateListItem(mli) + + if not mli: + return + + if not mli.dataSource.exists(): + try: + control.removeItem(mli.pos()) + except (ValueError, TypeError): + # fixme: why? + pass + + if not control.size(): + idx = self.hubFocusIndexes[hubControlID - 400] + while idx > 0: + idx -= 1 + controlID = 400 + self.hubFocusIndexes.index(idx) + control = self.hubControls[self.hubFocusIndexes.index(idx)] + if control.size(): + self.setFocusId(controlID) + break + else: + self.setFocusId(self.SECTION_LIST_ID) + + self.processCommand(command) + + def processCommand(self, command): + if command.startswith('HOME:'): + sectionID = command.split(':', 1)[-1] + for mli in self.sectionList: + if mli.dataSource and mli.dataSource.key == sectionID: + self.sectionList.selectItem(mli.pos()) + self.lastSection = mli.dataSource + self.sectionChanged() + + def checkSectionItem(self, force=False, action=None): + item = self.sectionList.getSelectedItem() + if not item: + return + + if not item.getProperty('item'): + if action and action == xbmcgui.ACTION_MOVE_RIGHT: + self.sectionList.selectItem(0) + item = self.sectionList[0] + else: + self.sectionList.selectItem(self.bottomItem) + item = self.sectionList[self.bottomItem] + + if item.dataSource != self.lastSection: + self.lastSection = item.dataSource + self.sectionChanged(force) + + def checkHubItem(self, controlID): + control = self.hubControls[controlID - 400] + mli = control.getSelectedItem() + is_valid_mli = mli and mli.getProperty('is.end') != '1' + + if util.advancedSettings.dynamicBackgrounds and is_valid_mli: + self.updateBackgroundFrom(mli.dataSource) + + if not mli or not mli.getProperty('is.end') or mli.getProperty('is.updating') == '1': + return + + mli.setBoolProperty('is.updating', True) + self.cleanTasks() + task = ExtendHubTask().setup(control.dataSource, self.extendHubCallback, + canceledCallback=lambda hub: mli.setBoolProperty('is.updating', False)) + self.tasks.append(task) + backgroundthread.BGThreader.addTask(task) + + def displayServerAndUser(self, **kwargs): + title = plexapp.ACCOUNT.title or plexapp.ACCOUNT.username or ' ' + self.setProperty('user.name', title) + self.setProperty('user.avatar', plexapp.ACCOUNT.thumb) + self.setProperty('user.avatar.letter', title[0].upper()) + + if plexapp.SERVERMANAGER.selectedServer: + self.setProperty('server.name', plexapp.SERVERMANAGER.selectedServer.name) + self.setProperty('server.icon', + 'script.plex/home/device/plex.png') # TODO: Set dynamically to whatever it should be if that's how it even works :) + self.setProperty('server.iconmod', + plexapp.SERVERMANAGER.selectedServer.isSecure and 'script.plex/home/device/lock.png' or '') + self.setProperty('server.iconmod2', + plexapp.SERVERMANAGER.selectedServer.isLocal and 'script.plex/home/device/home_small.png' + or '') + else: + self.setProperty('server.name', T(32338, 'No Servers Found')) + self.setProperty('server.icon', 'script.plex/home/device/error.png') + self.setProperty('server.iconmod', '') + self.setProperty('server.iconmod2', '') + + def cleanTasks(self): + self.tasks = [t for t in self.tasks if t.isValid()] + + def sectionChanged(self, force=False): + self.sectionChangeTimeout = time.time() + 0.3 + if not self.sectionChangeThread or not self.sectionChangeThread.is_alive() or force: + self.sectionChangeThread = threading.Thread(target=self._sectionChanged, name="sectionchanged") + self.sectionChangeThread.start() + + def _sectionChanged(self): + while not util.MONITOR.waitForAbort(0.1): + if time.time() >= self.sectionChangeTimeout: + break + + self._sectionReallyChanged() + + def _sectionReallyChanged(self): + with self.lock: + section = self.lastSection + self.setProperty('hub.focus', '') + if util.advancedSettings.dynamicBackgrounds: + self.backgroundSet = False + + util.DEBUG_LOG('Section changed ({0}): {1}'.format(section.key, repr(section.title))) + self.showHubs(section) + self.lastSection = section + self.checkSectionItem(force=True) + + def sectionHubsCallback(self, section, hubs): + with self.lock: + update = bool(self.sectionHubs.get(section.key)) + self.sectionHubs[section.key] = hubs + if self.lastSection == section: + self.showHubs(section, update=update) + + def updateHubCallback(self, hub, items=None): + with self.lock: + for mli in self.sectionList: + section = mli.dataSource + if not section: + continue + + hubs = self.sectionHubs.get(section.key, ()) + for idx, ihub in enumerate(hubs): + if ihub == hub: + if self.lastSection == section: + util.DEBUG_LOG('Hub {0} updated - refreshing section: {1}'.format(hub.hubIdentifier, repr(section.title))) + hubs[idx] = hub + self.showHub(hub, items=items) + return + + def extendHubCallback(self, hub, items): + util.DEBUG_LOG('ExtendHub called: {0} [{1}]'.format(hub.hubIdentifier, len(hub.items))) + self.updateHubCallback(hub, items) + + def showSections(self): + self.sectionHubs = {} + items = [] + + homemli = kodigui.ManagedListItem(T(32332, 'Home'), data_source=HomeSection) + homemli.setProperty('is.home', '1') + homemli.setProperty('item', '1') + items.append(homemli) + + pl = plexapp.SERVERMANAGER.selectedServer.playlists() + if pl: + plli = kodigui.ManagedListItem('Playlists', thumbnailImage='script.plex/home/type/playlists.png', data_source=PlaylistsSection) + plli.setProperty('is.playlists', '1') + plli.setProperty('item', '1') + items.append(plli) + + try: + sections = plexapp.SERVERMANAGER.selectedServer.library.sections() + except plexnet.exceptions.BadRequest: + self.setFocusId(self.SERVER_BUTTON_ID) + util.messageDialog("Error", "Bad request") + return + + if plexapp.SERVERMANAGER.selectedServer.hasHubs(): + self.tasks = [SectionHubsTask().setup(s, self.sectionHubsCallback) for s in [HomeSection, PlaylistsSection] + sections] + backgroundthread.BGThreader.addTasks(self.tasks) + + for section in sections: + mli = kodigui.ManagedListItem(section.title, thumbnailImage='script.plex/home/type/{0}.png'.format(section.type), data_source=section) + mli.setProperty('item', '1') + items.append(mli) + + self.bottomItem = len(items) - 1 + + for x in range(len(items), 8): + mli = kodigui.ManagedListItem() + items.append(mli) + + self.lastSection = HomeSection + self.sectionList.reset() + self.sectionList.addItems(items) + + if items: + self.setFocusId(self.SECTION_LIST_ID) + else: + self.setFocusId(self.SERVER_BUTTON_ID) + + def showHubs(self, section=None, update=False): + self.setBoolProperty('no.content', False) + if not update: + self.setProperty('drawing', '1') + try: + self._showHubs(section=section, update=update) + finally: + self.setProperty('drawing', '') + + def _showHubs(self, section=None, update=False): + if not update: + self.clearHubs() + + if not plexapp.SERVERMANAGER.selectedServer.hasHubs(): + return + + if section.key is False: + self.showBusy(False) + return + + self.showBusy(True) + + hubs = self.sectionHubs.get(section.key) + if hubs is False: + self.showBusy(False) + self.setBoolProperty('no.content', True) + return + + if not hubs: + for task in self.tasks: + if task.section == section: + backgroundthread.BGThreader.moveToFront(task) + break + + if section.type != "home": + self.showBusy(False) + self.setBoolProperty('no.content', True) + return + + if time.time() - hubs.lastUpdated > HUBS_REFRESH_INTERVAL: + util.DEBUG_LOG('Section is stale: REFRESHING - update: {0}'.format(update)) + hubs.lastUpdated = time.time() + self.cleanTasks() + if not update: + if section.key in self.sectionHubs: + self.sectionHubs[section.key] = None + self.tasks.append(SectionHubsTask().setup(section, self.sectionHubsCallback)) + backgroundthread.BGThreader.addTask(self.tasks[-1]) + return + + util.DEBUG_LOG('Showing hubs - Section: {0} - Update: {1}'.format(section.key, update)) + try: + hasContent = False + skip = {} + for hub in hubs: + identifier = hub.getCleanHubIdentifier() + + if identifier not in self.HUBMAP: + util.DEBUG_LOG('UNHANDLED - Hub: {0} [{1}]({2})'.format(hub.hubIdentifier, identifier, len(hub.items))) + continue + + skip[self.HUBMAP[identifier]['index']] = 1 + + if self.showHub(hub): + if hub.items: + hasContent = True + if self.HUBMAP[identifier].get('do_updates'): + self.updateHubs[identifier] = hub + + if not hasContent: + self.setBoolProperty('no.content', True) + + lastSkip = 0 + if skip: + lastSkip = min(skip.keys()) + + focus = None + if update: + for i, control in enumerate(self.hubControls): + if i in skip: + lastSkip = i + continue + if self.getFocusId() == control.getId(): + focus = lastSkip + control.reset() + + if focus is not None: + self.setFocusId(focus) + finally: + self.showBusy(False) + + def showHub(self, hub, items=None): + identifier = hub.getCleanHubIdentifier() + + if identifier in self.HUBMAP: + util.DEBUG_LOG('HUB: {0} [{1}]({2}, {3})'.format(hub.hubIdentifier, + identifier, + len(hub.items), + len(items) if items else None)) + self._showHub(hub, hubitems=items, **self.HUBMAP[identifier]) + return True + else: + util.DEBUG_LOG('UNHANDLED - Hub: {0} [{1}]({1})'.format(hub.hubIdentifier, identifier, len(hub.items))) + return + + def createGrandparentedListItem(self, obj, thumb_w, thumb_h, with_grandparent_title=False): + if with_grandparent_title and obj.get('grandparentTitle') and obj.title: + title = u'{0} - {1}'.format(obj.grandparentTitle, obj.title) + else: + title = obj.get('grandparentTitle') or obj.get('parentTitle') or obj.title or '' + mli = kodigui.ManagedListItem(title, thumbnailImage=obj.defaultThumb.asTranscodedImageURL(thumb_w, thumb_h), data_source=obj) + return mli + + def createParentedListItem(self, obj, thumb_w, thumb_h, with_parent_title=False): + if with_parent_title and obj.parentTitle and obj.title: + title = u'{0} - {1}'.format(obj.parentTitle, obj.title) + else: + title = obj.parentTitle or obj.title or '' + mli = kodigui.ManagedListItem(title, thumbnailImage=obj.defaultThumb.asTranscodedImageURL(thumb_w, thumb_h), data_source=obj) + return mli + + def createSimpleListItem(self, obj, thumb_w, thumb_h): + mli = kodigui.ManagedListItem(obj.title or '', thumbnailImage=obj.defaultThumb.asTranscodedImageURL(thumb_w, thumb_h), data_source=obj) + return mli + + def createEpisodeListItem(self, obj, wide=False): + mli = self.createGrandparentedListItem(obj, *self.THUMB_POSTER_DIM) + if obj.index: + subtitle = u'{0}{1} \u2022 {2}{3}'.format(T(32310, 'S'), obj.parentIndex, T(32311, 'E'), obj.index) + else: + subtitle = obj.originallyAvailableAt.asDatetime('%m/%d/%y') + + if wide: + mli.setLabel2(u'{0} - {1}'.format(util.shortenText(obj.title, 35), subtitle)) + else: + mli.setLabel2(subtitle) + + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/show.png') + if not obj.isWatched: + mli.setProperty('unwatched', '1') + return mli + + def createSeasonListItem(self, obj, wide=False): + mli = self.createParentedListItem(obj, *self.THUMB_POSTER_DIM) + # mli.setLabel2('Season {0}'.format(obj.index)) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/show.png') + if not obj.isWatched: + mli.setProperty('unwatched.count', str(obj.unViewedLeafCount)) + return mli + + def createMovieListItem(self, obj, wide=False): + mli = kodigui.ManagedListItem(obj.defaultTitle, obj.year, thumbnailImage=obj.defaultThumb.asTranscodedImageURL(*self.THUMB_POSTER_DIM), data_source=obj) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/movie.png') + if not obj.isWatched: + mli.setProperty('unwatched', '1') + return mli + + def createShowListItem(self, obj, wide=False): + mli = self.createSimpleListItem(obj, *self.THUMB_POSTER_DIM) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/show.png') + if not obj.isWatched: + mli.setProperty('unwatched.count', str(obj.unViewedLeafCount)) + return mli + + def createAlbumListItem(self, obj, wide=False): + mli = self.createParentedListItem(obj, *self.THUMB_SQUARE_DIM) + mli.setLabel2(obj.title) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/music.png') + return mli + + def createTrackListItem(self, obj, wide=False): + mli = self.createGrandparentedListItem(obj, *self.THUMB_SQUARE_DIM) + mli.setLabel2(obj.title) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/music.png') + return mli + + def createPhotoListItem(self, obj, wide=False): + mli = self.createSimpleListItem(obj, *self.THUMB_SQUARE_DIM) + if obj.type == 'photo': + mli.setLabel2(obj.originallyAvailableAt.asDatetime('%d %B %Y')) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/photo.png') + return mli + + def createClipListItem(self, obj, wide=False): + mli = self.createGrandparentedListItem(obj, *self.THUMB_AR16X9_DIM, with_grandparent_title=True) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/movie16x9.png') + return mli + + def createArtistListItem(self, obj, wide=False): + mli = self.createSimpleListItem(obj, *self.THUMB_SQUARE_DIM) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/music.png') + return mli + + def createPlaylistListItem(self, obj, wide=False): + if obj.playlistType == 'audio': + w, h = self.THUMB_SQUARE_DIM + thumb = obj.buildComposite(width=w, height=h, media='thumb') + else: + w, h = self.THUMB_AR16X9_DIM + thumb = obj.buildComposite(width=w, height=h, media='art') + + mli = kodigui.ManagedListItem( + obj.title or '', + util.durationToText(obj.duration.asInt()), + # thumbnailImage=obj.composite.asTranscodedImageURL(*self.THUMB_DIMS[obj.playlistType]['item.thumb']), + thumbnailImage=thumb, + data_source=obj + ) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/{0}.png'.format(obj.playlistType == 'audio' and 'music' or 'movie')) + return mli + + def unhandledHub(self, self2, obj, wide=False): + util.DEBUG_LOG('Unhandled Hub item: {0}'.format(obj.type)) + + CREATE_LI_MAP = { + 'episode': createEpisodeListItem, + 'season': createSeasonListItem, + 'movie': createMovieListItem, + 'show': createShowListItem, + 'album': createAlbumListItem, + 'track': createTrackListItem, + 'photo': createPhotoListItem, + 'photodirectory': createPhotoListItem, + 'clip': createClipListItem, + 'artist': createArtistListItem, + 'playlist': createPlaylistListItem + } + + def createListItem(self, obj, wide=False): + return self.CREATE_LI_MAP.get(obj.type, self.unhandledHub)(self, obj, wide) + + def clearHubs(self): + for control in self.hubControls: + control.reset() + + def _showHub(self, hub, hubitems=None, index=None, with_progress=False, with_art=False, ar16x9=False, + text2lines=False, **kwargs): + control = self.hubControls[index] + control.dataSource = hub + + if not hub.items and not hubitems: + control.reset() + return + + if not hubitems: + hub.reset() + + self.setProperty('hub.4{0:02d}'.format(index), hub.title or kwargs.get('title')) + self.setProperty('hub.text2lines.4{0:02d}'.format(index), text2lines and '1' or '') + + items = [] + + for obj in hubitems or hub.items: + if not self.backgroundSet: + if self.updateBackgroundFrom(obj): + self.backgroundSet = True + mli = self.createListItem(obj, wide=with_art) + if mli: + items.append(mli) + + if with_progress: + for mli in items: + mli.setProperty('progress', util.getProgressImage(mli.dataSource)) + if with_art: + for mli in items: + thumb = (util.advancedSettings.continueUseThumb + and mli.dataSource.type == 'episode' + and mli.dataSource.thumb + ) \ + or mli.dataSource.art + mli.setThumbnailImage(thumb.asTranscodedImageURL(*self.THUMB_AR16X9_DIM)) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/movie16x9.png') + if ar16x9: + for mli in items: + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/movie16x9.png') + + if hub.more.asBool(): + end = kodigui.ManagedListItem('') + end.setBoolProperty('is.end', True) + items.append(end) + + if hubitems: + end = control.size() - 1 + control.replaceItem(end, items[0]) + control.addItems(items[1:]) + control.selectItem(end) + else: + control.replaceItems(items) + + def updateListItem(self, mli): + if not mli or not mli.dataSource: # May have become invalid + return + + obj = mli.dataSource + if obj.type in ('episode', 'movie'): + mli.setProperty('unwatched', not obj.isWatched and '1' or '') + elif obj.type in ('season', 'show', 'album'): + if obj.isWatched: + mli.setProperty('unwatched.count', '') + else: + mli.setProperty('unwatched.count', str(obj.unViewedLeafCount)) + + def sectionClicked(self): + item = self.sectionList.getSelectedItem() + if not item: + return + + section = item.dataSource + + if section.type in ('show', 'movie', 'artist', 'photo'): + self.processCommand(opener.sectionClicked(section)) + elif section.type in ('playlists',): + self.processCommand(opener.handleOpen(playlists.PlaylistsWindow)) + + def onNewServer(self, **kwargs): + self.showServers(from_refresh=True) + + def onRemoveServer(self, **kwargs): + self.onNewServer() + + def onReachableServer(self, server=None, **kwargs): + for mli in self.serverList: + if mli.uuid == server.uuid: + mli.unHookSignals() + mli.dataSource = server + mli.hookSignals() + mli.onUpdate() + return + else: + self.onNewServer() + + def onSelectedServerChange(self, **kwargs): + if self.serverRefresh(): + self.setFocusId(self.SECTION_LIST_ID) + self.changingServer = False + + def showServers(self, from_refresh=False, mouse=False): + with self.lock: + selection = None + if from_refresh: + mli = self.serverList.getSelectedItem() + if mli: + selection = mli.uuid + + servers = sorted( + plexapp.SERVERMANAGER.getServers(), + key=lambda x: (x.owned and '0' or '1') + x.name.lower() + ) + + items = [] + for s in servers: + item = ServerListItem(s.name, not s.owned and s.owner or '', data_source=s) + item.uuid = s.uuid + item.onUpdate() + item.setProperty('current', plexapp.SERVERMANAGER.selectedServer.uuid == s.uuid and '1' or '') + items.append(item) + + if len(items) > 1: + items[0].setProperty('first', '1') + elif items: + items[0].setProperty('only', '1') + + self.serverList.replaceItems(items) + + self.getControl(800).setHeight((min(len(items), 9) * 100) + 80) + + for item in items: + if item.dataSource != kodigui.DUMMY_DATA_SOURCE: + item.hookSignals() + + if selection: + for mli in self.serverList: + if mli.uuid == selection: + self.serverList.selectItem(mli.pos()) + + if not from_refresh and items and not mouse: + self.setFocusId(self.SERVER_LIST_ID) + + if not from_refresh: + plexapp.refreshResources() + + def selectServer(self): + if self._shuttingDown: + return + + mli = self.serverList.getSelectedItem() + if not mli: + return + + self.changingServer = True + + # this is broken + with busy.BusySignalContext(plexapp.util.APP, "change:selectedServer") as bc: + self.setFocusId(self.SECTION_LIST_ID) + + server = mli.dataSource + + # fixme: this might still trigger a dialog, re-triggering the previously opened windows + if not self._shuttingDown and not server.isReachable(): + if server.pendingReachabilityRequests > 0: + util.messageDialog(T(32339, 'Server is not accessible'), T(32340, 'Connection tests are in ' + 'progress. Please wait.')) + else: + util.messageDialog( + T(32339, 'Server is not accessible'), T(32341, 'Server is not accessible. Please sign into ' + 'your server and check your connection.') + ) + bc.ignoreSignal = True + return + + changed = plexapp.SERVERMANAGER.setSelectedServer(server, force=True) + if not changed: + bc.ignoreSignal = True + self.changingServer = False + + def showUserMenu(self, mouse=False): + items = [] + if plexapp.ACCOUNT.isSignedIn: + if len(plexapp.ACCOUNT.homeUsers) > 1: + items.append(kodigui.ManagedListItem(T(32342, 'Switch User'), data_source='switch')) + items.append(kodigui.ManagedListItem(T(32343, 'Settings'), data_source='settings')) + if plexapp.ACCOUNT.isSignedIn: + items.append(kodigui.ManagedListItem(T(32344, 'Sign Out'), data_source='signout')) + elif plexapp.ACCOUNT.isOffline: + items.append(kodigui.ManagedListItem(T(32459, 'Offline Mode'), data_source='go_online')) + else: + items.append(kodigui.ManagedListItem(T(32460, 'Sign In'), data_source='signin')) + + if len(items) > 1: + items[0].setProperty('first', '1') + items[-1].setProperty('last', '1') + else: + items[0].setProperty('only', '1') + + self.userList.reset() + self.userList.addItems(items) + + self.getControl(801).setHeight((len(items) * 66) + 80) + + if not mouse: + self.setFocusId(self.USER_LIST_ID) + + def doUserOption(self): + mli = self.userList.getSelectedItem() + if not mli: + return + + option = mli.dataSource + + self.setFocusId(self.USER_BUTTON_ID) + + if option == 'settings': + from . import settings + settings.openWindow() + elif option == 'go_online': + plexapp.ACCOUNT.refreshAccount() + else: + self.closeOption = option + self.doClose() + + def showAudioPlayer(self): + from . import musicplayer + self.processCommand(opener.handleOpen(musicplayer.MusicPlayerWindow)) + + def finished(self): + if self.tasks: + for task in self.tasks: + task.cancel() diff --git a/script.plexmod/lib/windows/info.py b/script.plexmod/lib/windows/info.py new file mode 100644 index 000000000..4c3374cd2 --- /dev/null +++ b/script.plexmod/lib/windows/info.py @@ -0,0 +1,149 @@ +from __future__ import absolute_import +from . import kodigui +from . import windowutils +from lib import util +from plexnet.video import Episode, Movie, Clip + +import os + + +def split2len(s, n): + def _f(s, n): + while s: + yield s[:n] + s = s[n:] + return list(_f(s, n)) + + +class InfoWindow(kodigui.ControlledWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-info.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + PLAYER_STATUS_BUTTON_ID = 204 + + THUMB_DIM_POSTER = util.scaleResolution(519, 469) + THUMB_DIM_SQUARE = util.scaleResolution(519, 519) + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.title = kwargs.get('title') + self.subTitle = kwargs.get('sub_title') + self.thumb = kwargs.get('thumb') + self.thumbFallback = kwargs.get('thumb_fallback') + self.info = kwargs.get('info') + self.background = kwargs.get('background') + self.isSquare = kwargs.get('is_square') + self.is16x9 = kwargs.get('is_16x9') + self.isPoster = not (self.isSquare or self.is16x9) + self.thumbDim = self.isSquare and self.THUMB_DIM_SQUARE or self.THUMB_DIM_POSTER + self.video = kwargs.get('video') + + def getVideoInfo(self): + """ + Append media/part/stream info to summary + """ + if not isinstance(self.video, (Episode, Movie, Clip)): + return self.info + + summary = [self.info] + medias = self.video.media() + mediaCount = len(medias) + onlyOneMedia = mediaCount == 1 + partCount = sum(len(m.parts) for m in medias) + pcInfo = [] + if not onlyOneMedia: + pcInfo.append("Files: {}".format(mediaCount)) + if partCount > 1: + pcInfo.append("Parts: {}".format(partCount)) + pcInfoStr = ", ".join(pcInfo) + + addMedia = ["\n\n\n\nMedia{}\n".format(" ({})".format(pcInfoStr) if pcInfoStr else "")] + for media_ in medias: + if not media_.isAccessible(): + addMedia.append("Unavailable: {}\n\n".format(", ".join(os.path.basename(pf.file) for pf in media_.parts))) + continue + + for part in media_.parts: + if not part: + addMedia.append("Unavailable: {}".format(os.path.basename(part.file))) + continue + + addMedia.append("File: ") + splitFnAt = 74 + fnLen = len(os.path.basename(part.file)) + appended = False + for s in split2len(os.path.basename(part.file), splitFnAt): + if fnLen > splitFnAt and not appended: + addMedia.append("{}\n".format(s)) + appended = True + continue + addMedia.append("{}\n".format(s)) + addMedia.append("Duration: {}, Size: {}\n".format(util.durationToShortText(int(part.duration)), + util.simpleSize(int(part.size)))) + + subs = [] + subsOver = 0 + for stream in part.streams: + streamtype = stream.streamType.asInt() + # video + if streamtype == 1: + addMedia.append("Video: {}x{}, {} {}/{}bit/{}/{}@{} kBit, {} fps\n".format( + stream.width, stream.height, stream.videoCodecRendering, stream.codec.upper(), + stream.bitDepth, stream.chromaSubsampling, stream.colorPrimaries, stream.bitrate, + stream.frameRate)) + # audio + elif streamtype == 2: + addMedia.append("Audio: {}{}, {}/{}ch@{} kBit, {} Hz\n".format( + stream.language, + " (default)" if stream.default else "", + stream.codec.upper(), + stream.channels, stream.bitrate, + stream.samplingRate)) + # subtitle + elif streamtype == 3: + if len(subs) > 4: + subsOver += 1 + continue + subs.append("{} ({})".format(stream.language, stream.codec.upper())) + + if subs: + addMedia.append("Subtitles: {}{}\n".format(", ".join(subs), + subsOver and " (+{})".format(subsOver) or '')) + if not onlyOneMedia: + addMedia.append("--------------\n") + + chapters = [] + chOver = 0 + for index, chapter in enumerate(self.video.chapters): + if len(chapters) > 4: + chOver += 1 + continue + chapters.append(chapter.tag or "Chapter #{}".format(str(index+1))) + + if chapters: + addMedia.append("Chapters: {}{}\n".format(", ".join(chapters), chOver and " (+{})".format(chOver) or '')) + + if self.video.markers: + addMedia.append("Markers: {}".format(", ".join(name for off, name in sorted( + (int(marker.startTimeOffset), marker.type) for marker in self.video.markers)))) + + return "".join(summary + addMedia) + + def onFirstInit(self): + self.setProperty('is.poster', self.isPoster and '1' or '') + self.setProperty('is.square', self.isSquare and '1' or '') + self.setProperty('is.16x9', self.is16x9 and '1' or '') + self.setProperty('title.main', self.title) + self.setProperty('title.sub', self.subTitle) + self.setProperty('thumb.fallback', self.thumbFallback) + self.setProperty('thumb', self.thumb.asTranscodedImageURL(*self.thumbDim)) + self.setProperty('info', self.getVideoInfo()) + self.setProperty('background', self.background) + + def onClick(self, controlID): + if controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() diff --git a/script.plexmod/lib/windows/kodigui.py b/script.plexmod/lib/windows/kodigui.py new file mode 100644 index 000000000..379633920 --- /dev/null +++ b/script.plexmod/lib/windows/kodigui.py @@ -0,0 +1,1105 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +import time +import threading +import traceback +from six.moves import range +from six.moves import zip +from .. import util + +MONITOR = None + + +class BaseFunctions: + xmlFile = '' + path = '' + theme = '' + res = '720p' + width = 1280 + height = 720 + + usesGenerate = False + lastWinID = None + lastDialogID = None + + def __init__(self): + self.isOpen = True + + def onWindowFocus(self): + # Not automatically called. Can be used by an external window manager + pass + + def onClosed(self): + pass + + @classmethod + def open(cls, **kwargs): + window = cls(cls.xmlFile, cls.path, cls.theme, cls.res, **kwargs) + window.modal() + return window + + @classmethod + def create(cls, show=True, **kwargs): + window = cls(cls.xmlFile, cls.path, cls.theme, cls.res, **kwargs) + if show: + window.show() + if xbmcgui.getCurrentWindowId() < 13000: + window.isOpen = False + return window + + window.isOpen = xbmcgui.getCurrentWindowId() >= 13000 + return window + + def modal(self): + self.isOpen = True + try: + self.doModal() + except SystemExit: + pass + self.onClosed() + self.isOpen = False + + def activate(self): + if not self._winID: + self._winID = xbmcgui.getCurrentWindowId() + xbmc.executebuiltin('ReplaceWindow({0})'.format(self._winID)) + + def mouseXTrans(self, val): + return int((val / self.getWidth()) * self.width) + + def mouseYTrans(self, val): + return int((val / self.getHeight()) * self.height) + + def closing(self): + return self._closing + + @classmethod + def generate(self): + return None + + def setProperties(self, prop_list, val_list_or_val): + if isinstance(val_list_or_val, list) or isinstance(val_list_or_val, tuple): + val_list = val_list_or_val + else: + val_list = [val_list_or_val] * len(prop_list) + + for prop, val in zip(prop_list, val_list): + self.setProperty(prop, val) + + def propertyContext(self, prop, val='1'): + return WindowProperty(self, prop, val) + + def setBoolProperty(self, key, boolean): + self.setProperty(key, boolean and '1' or '') + + +LAST_BG_URL = None + + +class BaseWindow(xbmcgui.WindowXML, BaseFunctions): + def __init__(self, *args, **kwargs): + BaseFunctions.__init__(self) + self._closing = False + self._winID = None + self.started = False + self.finishedInit = False + + def onInit(self): + global LAST_BG_URL + self._winID = xbmcgui.getCurrentWindowId() + BaseFunctions.lastWinID = self._winID + self.setProperty('use_solid_background', util.hasCustomBGColour and '1' or '') + if util.hasCustomBGColour: + bgColour = util.advancedSettings.backgroundColour if util.advancedSettings.backgroundColour != "-" \ + else "ff000000" + self.setProperty('background_colour', "0x%s" % bgColour.lower()) + self.setProperty('background_colour_opaque', "0x%s" % bgColour.lower()) + else: + # set background color to 0 to avoid kodi UI BG clearing, improves performance + if util.advancedSettings.dbgCrossfade: + self.setProperty('background_colour', "0x00000000") + else: + self.setProperty('background_colour', "0xff111111") + self.setProperty('background_colour_opaque', "0xff111111") + + self.setBoolProperty('use_bg_fallback', util.advancedSettings.useBgFallback) + + try: + if self.started: + self.onReInit() + else: + self.started = True + if LAST_BG_URL: + self.windowSetBackground(LAST_BG_URL) + self.onFirstInit() + self.finishedInit = True + except util.NoDataException: + self.exitCommand = "NODATA" + self.doClose() + + def onFirstInit(self): + pass + + def onReInit(self): + pass + + def setProperty(self, key, value): + if self._closing: + return + + if not self._winID: + self._winID = xbmcgui.getCurrentWindowId() + + try: + xbmcgui.Window(self._winID).setProperty(key, value) + xbmcgui.WindowXML.setProperty(self, key, value) + except RuntimeError: + xbmc.log('kodigui.BaseWindow.setProperty: Missing window', xbmc.LOGDEBUG) + + def updateBackgroundFrom(self, ds): + if util.advancedSettings.dynamicBackgrounds: + return self.windowSetBackground(util.backgroundFromArt(ds.art, width=self.width, + height=self.height)) + + def windowSetBackground(self, value): + if not util.advancedSettings.dbgCrossfade: + if not value: + return + self.setProperty("background_static", value) + return value + + global LAST_BG_URL + + if not value: + if LAST_BG_URL: + self.setProperty("background_static", LAST_BG_URL) + return LAST_BG_URL + return + + cur1 = self.getProperty('background') + if not cur1: + self.setProperty("background_static", value) + self.setProperty("background", value) + + elif LAST_BG_URL != value: + self.setProperty("background_static", LAST_BG_URL) + self.setProperty("background", value) + + LAST_BG_URL = value + return value + + def doClose(self): + if not self.isOpen: + return + self._closing = True + self.isOpen = False + self.close() + + def show(self): + self._closing = False + #self.isOpen = True + xbmcgui.WindowXML.show(self) + self.isOpen = xbmcgui.getCurrentWindowId() >= 13000 + + def onClosed(self): + pass + + +class BaseDialog(xbmcgui.WindowXMLDialog, BaseFunctions): + def __init__(self, *args, **kwargs): + BaseFunctions.__init__(self) + self._closing = False + self._winID = '' + self.started = False + + def onInit(self): + self._winID = xbmcgui.getCurrentWindowDialogId() + BaseFunctions.lastDialogID = self._winID + if self.started: + self.onReInit() + else: + self.started = True + self.onFirstInit() + + def onFirstInit(self): + pass + + def onReInit(self): + pass + + def setProperty(self, key, value): + if self._closing: + return + + if not self._winID: + self._winID = xbmcgui.getCurrentWindowId() + + try: + xbmcgui.Window(self._winID).setProperty(key, value) + xbmcgui.WindowXMLDialog.setProperty(self, key, value) + except RuntimeError: + xbmc.log('kodigui.BaseDialog.setProperty: Missing window', xbmc.LOGDEBUG) + + def doClose(self): + self._closing = True + self.close() + self.isOpen = False + + def show(self): + self._closing = False + xbmcgui.WindowXMLDialog.show(self) + self.isOpen = True + + def onClosed(self): + pass + + +class ControlledBase: + def doModal(self): + self.show() + self.wait() + + def wait(self): + while not self._closing and not MONITOR.waitForAbort(0.1): + pass + + def close(self): + self._closing = True + + +class ControlledWindow(ControlledBase, BaseWindow): + def onAction(self, action): + try: + if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): + self.doClose() + return + except: + traceback.print_exc() + + BaseWindow.onAction(self, action) + + +class ControlledDialog(ControlledBase, BaseDialog): + def onAction(self, action): + try: + if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): + self.doClose() + return + except: + traceback.print_exc() + + BaseDialog.onAction(self, action) + + +DUMMY_LIST_ITEM = xbmcgui.ListItem() + + +class DummyDataSource(object): + def __nonzero__(self): + return False + + __bool__ = __nonzero__ + + def exists(self): + return False + + +DUMMY_DATA_SOURCE = DummyDataSource() + + +class ManagedListItem(object): + def __init__(self, label='', label2='', iconImage='', thumbnailImage='', path='', data_source=None, properties=None): + self._listItem = xbmcgui.ListItem(label, label2, path=path) + self._listItem.setArt({"thumb": thumbnailImage, "icon": iconImage}) + self.dataSource = data_source + self.properties = {} + self.label = label + self.label2 = label2 + self.iconImage = iconImage + self.thumbnailImage = thumbnailImage + self.path = path + self._ID = None + self._manager = None + self._valid = True + if properties: + for k, v in properties.items(): + self.setProperty(k, v) + + def __nonzero__(self): + return self._valid + + @property + def listItem(self): + if not self._listItem: + if not self._manager: + return None + + try: + self._listItem = self._manager.getListItemFromManagedItem(self) + except RuntimeError: + return None + + return self._listItem + + def invalidate(self): + self._valid = False + self._listItem = DUMMY_LIST_ITEM + self.dataSource = DUMMY_DATA_SOURCE + + def _takeListItem(self, manager, lid): + self._manager = manager + self._ID = lid + self._listItem.setProperty('__ID__', lid) + li = self._listItem + self._listItem = None + self._manager._properties.update(self.properties) + return li + + def _updateListItem(self): + self.listItem.setProperty('__ID__', self._ID) + self.listItem.setLabel(self.label) + self.listItem.setLabel2(self.label2) + self.listItem.setArt({"thumb": self.thumbnailImage, "icon": self.iconImage}) + self.listItem.setPath(self.path) + for k in self._manager._properties.keys(): + self.listItem.setProperty(k, self.properties.get(k) or '') + + def clear(self): + self.label = '' + self.label2 = '' + self.iconImage = '' + self.thumbnailImage = '' + self.path = '' + for k in self.properties: + self.properties[k] = '' + self._updateListItem() + + def pos(self): + if not self._manager: + return None + return self._manager.getManagedItemPosition(self) + + def addContextMenuItems(self, items, replaceItems=False): + self.listItem.addContextMenuItems(items, replaceItems) + + def addStreamInfo(self, stype, values): + self.listItem.addStreamInfo(stype, values) + + def getLabel(self): + return self.label + + def getLabel2(self): + return self.label2 + + def getProperty(self, key): + return self.properties.get(key, '') + + def getdescription(self): + return self.listItem.getdescription() + + def getduration(self): + return self.listItem.getduration() + + def getfilename(self): + return self.listItem.getfilename() + + def isSelected(self): + return self.listItem.isSelected() + + def select(self, selected): + return self.listItem.select(selected) + + def setArt(self, values): + return self.listItem.setArt(values) + + def setIconImage(self, icon): + self.iconImage = icon + return self.listItem.setArt({"icon": self.iconImage}) + + def setInfo(self, itype, infoLabels): + return self.listItem.setInfo(itype, infoLabels) + + def setLabel(self, label): + self.label = label + return self.listItem.setLabel(label) + + def setLabel2(self, label): + self.label2 = label + return self.listItem.setLabel2(label) + + def setMimeType(self, mimetype): + return self.listItem.setMimeType(mimetype) + + def setPath(self, path): + self.path = path + return self.listItem.setPath(path) + + def setProperty(self, key, value): + if self._manager: + self._manager._properties[key] = 1 + self.properties[key] = value + self.listItem.setProperty(key, value) + return self + + def setBoolProperty(self, key, boolean): + return self.setProperty(key, boolean and '1' or '') + + def setSubtitles(self, subtitles): + return self.listItem.setSubtitles(subtitles) # List of strings - HELIX + + def setThumbnailImage(self, thumb): + self.thumbnailImage = thumb + return self.listItem.setArt({"thumb": self.thumbnailImage}) + + def onDestroy(self): + pass + + +class ManagedControlList(object): + def __init__(self, window, control_id, max_view_index, data_source=None): + self.controlID = control_id + self.control = window.getControl(control_id) + self.items = [] + self._sortKey = None + self._idCounter = 0 + self._maxViewIndex = max_view_index + self._properties = {} + self.dataSource = data_source + + def __getattr__(self, name): + return getattr(self.control, name) + + def __getitem__(self, idx): + if isinstance(idx, slice): + return self.items[idx] + else: + return self.getListItem(idx) + + def __iter__(self): + for i in self.items: + yield i + + def __len__(self): + return self.size() + + def _updateItems(self, bottom=None, top=None): + if bottom is None: + bottom = 0 + top = self.size() + + try: + for idx in range(bottom, top): + try: + li = self.control.getListItem(idx) + except RuntimeError: + continue + + mli = self.items[idx] + self._properties.update(mli.properties) + mli._manager = self + mli._listItem = li + mli._updateListItem() + mli.setProperty('index', str(idx)) + except RuntimeError: + #xbmc.log('kodigui.ManagedControlList._updateItems: Runtime error', xbmc.LOGINFO) + util.ERROR('kodigui.ManagedControlList._updateItems: Runtime error') + return False + + return True + + def _nextID(self): + self._idCounter += 1 + return str(self._idCounter) + + def reInit(self, window, control_id): + self.controlID = control_id + self.control = window.getControl(control_id) + self.control.addItems([i._takeListItem(self, self._nextID()) for i in self.items]) + + def setSort(self, sort): + self._sortKey = sort + + def addItem(self, managed_item): + self.items.append(managed_item) + self.control.addItem(managed_item._takeListItem(self, self._nextID())) + + def addItems(self, managed_items): + self.items += managed_items + self.control.addItems([i._takeListItem(self, self._nextID()) for i in managed_items]) + + def replaceItem(self, pos, mli): + self[pos].onDestroy() + self[pos].invalidate() + self.items[pos] = mli + li = self.control.getListItem(pos) + mli._manager = self + mli._listItem = li + mli._updateListItem() + + def replaceItems(self, managed_items): + if not self.items: + self.addItems(managed_items) + return True + + oldSize = self.size() + + for i in self.items: + i.onDestroy() + i.invalidate() + + self.items = managed_items + size = self.size() + if size != oldSize: + pos = self.getSelectedPosition() + + if size > oldSize: + for i in range(0, size - oldSize): + self.control.addItem(xbmcgui.ListItem()) + elif size < oldSize: + diff = oldSize - size + idx = oldSize - 1 + while diff: + self.control.removeItem(idx) + idx -= 1 + diff -= 1 + + if self.positionIsValid(pos): + self.selectItem(pos) + elif pos >= size: + self.selectItem(size - 1) + + return self._updateItems(0, self.size()) + + def getListItem(self, pos): + li = self.control.getListItem(pos) + mli = self.items[pos] + mli._listItem = li + return mli + + def getListItemByDataSource(self, data_source): + for mli in self: + if data_source == mli.dataSource: + return mli + return None + + def getSelectedItem(self): + pos = self.control.getSelectedPosition() + if not self.positionIsValid(pos): + pos = self.size() - 1 + + if pos < 0: + return None + return self.getListItem(pos) + + def setSelectedItemByPos(self, pos): + if self.positionIsValid(pos): + self.control.selectItem(pos) + + def removeItem(self, index): + old = self.items.pop(index) + old.onDestroy() + old.invalidate() + + self.control.removeItem(index) + top = self.control.size() - 1 + if top < 0: + return + if top < index: + index = top + self.control.selectItem(index) + + def removeManagedItem(self, mli): + self.removeItem(mli.pos()) + + def insertItem(self, index, managed_item): + pos = self.getSelectedPosition() + 1 + + if index >= self.size() or index < 0: + self.addItem(managed_item) + else: + self.items.insert(index, managed_item) + self.control.addItem(managed_item._takeListItem(self, self._nextID())) + self._updateItems(index, self.size()) + + if self.positionIsValid(pos): + self.selectItem(pos) + + def moveItem(self, mli, dest_idx): + source_idx = mli.pos() + if source_idx < dest_idx: + rstart = source_idx + rend = dest_idx + 1 + # dest_idx-=1 + else: + rstart = dest_idx + rend = source_idx + 1 + mli = self.items.pop(source_idx) + self.items.insert(dest_idx, mli) + + self._updateItems(rstart, rend) + + def swapItems(self, pos1, pos2): + if not self.positionIsValid(pos1) or not self.positionIsValid(pos2): + return False + + item1 = self.items[pos1] + item2 = self.items[pos2] + li1 = item1._listItem + li2 = item2._listItem + item1._listItem = li2 + item2._listItem = li1 + + item1._updateListItem() + item2._updateListItem() + self.items[pos1] = item2 + self.items[pos2] = item1 + + return True + + def shiftView(self, shift, hold_selected=False): + if not self._maxViewIndex: + return + selected = self.getSelectedItem() + selectedPos = selected.pos() + viewPos = self.getViewPosition() + + if shift > 0: + pushPos = selectedPos + (self._maxViewIndex - viewPos) + shift + if pushPos >= self.size(): + pushPos = self.size() - 1 + self.selectItem(pushPos) + newViewPos = self._maxViewIndex + elif shift < 0: + pushPos = (selectedPos - viewPos) + shift + if pushPos < 0: + pushPos = 0 + self.selectItem(pushPos) + newViewPos = 0 + + if hold_selected: + self.selectItem(selected.pos()) + else: + diff = newViewPos - viewPos + fix = pushPos - diff + # print '{0} {1} {2}'.format(newViewPos, viewPos, fix) + if self.positionIsValid(fix): + self.selectItem(fix) + + def reset(self): + self.dataSource = None + for i in self.items: + i.onDestroy() + i.invalidate() + self.items = [] + self.control.reset() + + def size(self): + return len(self.items) + + def getViewPosition(self): + try: + return int(xbmc.getInfoLabel('Container({0}).Position'.format(self.controlID))) + except: + return 0 + + def getViewRange(self): + viewPosition = self.getViewPosition() + selected = self.getSelectedPosition() + return list(range(max(selected - viewPosition, 0), min(selected + (self._maxViewIndex - viewPosition) + 1, self.size() - 1))) + + def positionIsValid(self, pos): + return 0 <= pos < self.size() + + def sort(self, sort=None, reverse=False): + sort = sort or self._sortKey + + self.items.sort(key=sort, reverse=reverse) + + self._updateItems(0, self.size()) + + def reverse(self): + self.items.reverse() + self._updateItems(0, self.size()) + + def getManagedItemPosition(self, mli): + return self.items.index(mli) + + def isLastItem(self, mli=None): + return self.getManagedItemPosition(mli or self.getSelectedItem()) + 1 == len(self) + + def getListItemFromManagedItem(self, mli): + pos = self.items.index(mli) + return self.control.getListItem(pos) + + def topHasFocus(self): + return self.getSelectedPosition() == 0 + + def bottomHasFocus(self): + return self.getSelectedPosition() == self.size() - 1 + + def invalidate(self): + for item in self.items: + item._listItem = DUMMY_LIST_ITEM + + def newControl(self, window=None, control_id=None): + self.controlID = control_id or self.controlID + self.control = window.getControl(self.controlID) + self.control.addItems([xbmcgui.ListItem() for i in range(self.size())]) + self._updateItems() + + +class _MWBackground(ControlledWindow): + def __init__(self, *args, **kwargs): + self._multiWindow = kwargs.get('multi_window') + self.started = False + BaseWindow.__init__(self, *args, **kwargs) + + def onInit(self): + if self.started: + return + self.started = True + self._multiWindow._open() + self.close() + + +class MultiWindow(object): + def __init__(self, windows=None, default_window=None, **kwargs): + self._windows = windows + self._next = default_window or self._windows[0] + self._properties = {} + self._current = None + self._allClosed = False + self.exitCommand = None + + def __getattr__(self, name): + return getattr(self._current, name) + + def setWindows(self, windows): + self._windows = windows + + def setDefault(self, default): + self._next = default or self._windows[0] + + def windowIndex(self, window): + if hasattr(window, 'MULTI_WINDOW_ID'): + for i, w in enumerate(self._windows): + if window.MULTI_WINDOW_ID == w.MULTI_WINDOW_ID: + return i + return 0 + else: + return self._windows.index(window.__class__) + + def nextWindow(self, window=None): + if window is False: + window = self._windows[self.windowIndex(self._current)] + + if window: + if window.__class__ == self._current.__class__: + return None + else: + idx = self.windowIndex(self._current) + idx += 1 + if idx >= len(self._windows): + idx = 0 + window = self._windows[idx] + + self._next = window + self._current.doClose() + return self._next + + def _setupCurrent(self, cls): + self._current = cls(cls.xmlFile, cls.path, cls.theme, cls.res) + self._current.onFirstInit = self._onFirstInit + self._current.onReInit = self.onReInit + self._current.onClick = self.onClick + self._current.onFocus = self.onFocus + + self._currentOnAction = self._current.onAction + self._current.onAction = self.onAction + + @classmethod + def open(cls, **kwargs): + mw = cls(**kwargs) + b = _MWBackground(mw.bgXML, mw.path, mw.theme, mw.res, multi_window=mw) + b.modal() + del b + import gc + gc.collect(2) + return mw + + def _open(self): + while not MONITOR.abortRequested() and not self._allClosed: + self._setupCurrent(self._next) + self._current.modal() + + self._current.doClose() + del self._current + del self._next + del self._currentOnAction + + def setProperty(self, key, value): + self._properties[key] = value + self._current.setProperty(key, value) + + def _onFirstInit(self): + for k, v in self._properties.items(): + self._current.setProperty(k, v) + self.onFirstInit() + + def doClose(self): + self._allClosed = True + self._current.doClose() + + def onFirstInit(self): + pass + + def onReInit(self): + pass + + def onAction(self, action): + if action == xbmcgui.ACTION_PREVIOUS_MENU or action == xbmcgui.ACTION_NAV_BACK: + self.doClose() + self._currentOnAction(action) + + def onClick(self, controlID): + pass + + def onFocus(self, controlID): + pass + + +class SafeControlEdit(object): + CHARS_LOWER = 'abcdefghijklmnopqrstuvwxyz' + CHARS_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + CHARS_NUMBERS = '0123456789' + CURSOR = '[COLOR FFCC7B19]|[/COLOR]' + + def __init__(self, control_id, label_id, window, key_callback=None, grab_focus=False): + self.controlID = control_id + self.labelID = label_id + self._win = window + self._keyCallback = key_callback + self.grabFocus = grab_focus + self._text = '' + self._compatibleMode = False + self.setup() + + def setup(self): + self._labelControl = self._win.getControl(self.labelID) + self._winOnAction = self._win.onAction + self._win.onAction = self.onAction + self.updateLabel() + + def setCompatibleMode(self, on): + self._compatibleMode = on + + def onAction(self, action): + try: + controlID = self._win.getFocusId() + if controlID == self.controlID: + if self.processAction(action.getId()): + return + elif self.grabFocus: + if self.processOffControlAction(action.getButtonCode()): + self._win.setFocusId(self.controlID) + return + except: + traceback.print_exc() + + self._winOnAction(action) + + def processAction(self, action_id): + if not self._compatibleMode: + oldVal = self._text + self._text = self._win.getControl(self.controlID).getText() + + if self._keyCallback: + self._keyCallback(action_id, oldVal, self._text) + + self.updateLabel() + + return True + oldVal = self.getText() + + if 61793 <= action_id <= 61818: # Lowercase + self.processChar(self.CHARS_LOWER[action_id - 61793]) + elif 61761 <= action_id <= 61786: # Uppercase + self.processChar(self.CHARS_UPPER[action_id - 61761]) + elif 61744 <= action_id <= 61753: + self.processChar(self.CHARS_NUMBERS[action_id - 61744]) + elif action_id == 61728: # Space + self.processChar(' ') + elif action_id == 61448: + self.delete() + else: + return False + + if self._keyCallback: + self._keyCallback(action_id, oldVal, self.getText()) + + return True + + def processOffControlAction(self, action_id): + oldVal = self.getText() if self._compatibleMode else self._text + if 61505 <= action_id <= 61530: # Lowercase + self.processChar(self.CHARS_LOWER[action_id - 61505]) + elif 192577 <= action_id <= 192602: # Uppercase + self.processChar(self.CHARS_UPPER[action_id - 192577]) + elif 61488 <= action_id <= 61497: + self.processChar(self.CHARS_NUMBERS[action_id - 61488]) + elif 61552 <= action_id <= 61561: + self.processChar(self.CHARS_NUMBERS[action_id - 61552]) + elif action_id == 61472: # Space + self.processChar(' ') + else: + return False + + if self._keyCallback: + self._keyCallback(action_id, oldVal, self.getText()) + + return True + + def _setText(self, text): + self._text = text + + if not self._compatibleMode: + self._win.getControl(self.controlID).setText(text) + self.updateLabel() + + def _getText(self): + if not self._compatibleMode and self._win.getFocusId() == self.controlID: + return self._win.getControl(self.controlID).getText() + else: + return self._text + + def updateLabel(self): + self._labelControl.setLabel(self._getText() + self.CURSOR) + + def processChar(self, char): + self._setText(self.getText() + char) + + def setText(self, text): + self._setText(text) + + def getText(self): + return self._getText() + + def append(self, text): + self._setText(self.getText() + text) + + def delete(self): + self._setText(self.getText()[:-1]) + + +class PropertyTimer(): + def __init__(self, window_id, timeout, property_, value='', init_value='1', addon_id=None, callback=None): + self._winID = window_id + self._timeout = timeout + self._property = property_ + self._value = value + self._initValue = init_value + self._endTime = 0 + self._thread = None + self._addonID = addon_id + self._closeWin = None + self._closed = False + self._callback = callback + + def _onTimeout(self): + self._endTime = 0 + xbmcgui.Window(self._winID).setProperty(self._property, self._value) + if self._addonID: + xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self._property), self._value) + if self._closeWin: + self._closeWin.doClose() + if self._callback: + self._callback() + + def _wait(self): + while not MONITOR.abortRequested() and time.time() < self._endTime: + xbmc.sleep(100) + if MONITOR.abortRequested(): + return + if self._endTime == 0: + return + self._onTimeout() + + def _stopped(self): + return not self._thread or not self._thread.is_alive() + + def _reset(self): + self._endTime = time.time() + self._timeout + + def _start(self): + self.init(self._initValue) + self._thread = threading.Thread(target=self._wait) + self._thread.start() + + def stop(self, trigger=False): + self._endTime = trigger and 1 or 0 + if not self._stopped(): + self._thread.join() + + def close(self): + self._closed = True + self.stop() + + def init(self, val): + if val is False: + return + elif val is None: + val = self._initValue + + xbmcgui.Window(self._winID).setProperty(self._property, val) + if self._addonID: + xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self._property), val) + + def reset(self, close_win=None, init=None): + self.init(init) + + if self._closed: + return + + if not self._timeout: + return + + self._closeWin = close_win + self._reset() + + if self._stopped: + self._start() + + +class WindowProperty(): + def __init__(self, win, prop, val='1', end=None): + self.win = win + self.prop = prop + self.val = val + self.end = end + self.old = self.win.getProperty(self.prop) + + def __enter__(self): + self.win.setProperty(self.prop, self.val) + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.win.setProperty(self.prop, self.end or self.old) + + +class GlobalProperty(): + def __init__(self, prop, val='1', end=None): + from kodi_six import xbmcaddon + self._addonID = xbmcaddon.Addon().getAddonInfo('id') + self.prop = prop + self.val = val + self.end = end + self.old = xbmc.getInfoLabel('Window(10000).Property({0}}.{1})'.format(self._addonID, prop)) + + def __enter__(self): + xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self.prop), self.val) + return self + + def __exit__(self, exc_type, exc_value, traceback): + xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self.prop), self.end or self.old) diff --git a/script.plexmod/lib/windows/library.py b/script.plexmod/lib/windows/library.py new file mode 100644 index 000000000..62245203a --- /dev/null +++ b/script.plexmod/lib/windows/library.py @@ -0,0 +1,1868 @@ +from __future__ import absolute_import +import os +import random +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import json +import time +import threading + +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from lib import util +from lib import backgroundthread +from lib import player + +from . import busy +from . import subitems +from . import preplay +from . import search +import plexnet +from . import dropdown +from . import opener +from . import windowutils + +from plexnet import playqueue + +from lib.util import T +import six +from six.moves import range + +CHUNK_SIZE = 200 +# CHUNK_SIZE = 30 + +KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + +MOVE_SET = frozenset( + ( + xbmcgui.ACTION_MOVE_LEFT, + xbmcgui.ACTION_MOVE_RIGHT, + xbmcgui.ACTION_MOVE_UP, + xbmcgui.ACTION_MOVE_DOWN, + xbmcgui.ACTION_MOUSE_MOVE, + xbmcgui.ACTION_PAGE_UP, + xbmcgui.ACTION_PAGE_DOWN, + xbmcgui.ACTION_FIRST_PAGE, + xbmcgui.ACTION_LAST_PAGE, + xbmcgui.ACTION_MOUSE_WHEEL_DOWN, + xbmcgui.ACTION_MOUSE_WHEEL_UP + ) +) + +THUMB_POSTER_DIM = util.scaleResolution(268, 397) +THUMB_AR16X9_DIM = util.scaleResolution(619, 348) +THUMB_SQUARE_DIM = util.scaleResolution(355, 355) +ART_AR16X9_DIM = util.scaleResolution(630, 355) + +TYPE_KEYS = { + 'episode': { + 'fallback': 'show', + 'thumb_dim': THUMB_POSTER_DIM, + }, + 'season': { + 'fallback': 'show', + 'thumb_dim': THUMB_POSTER_DIM + }, + 'movie': { + 'fallback': 'movie', + 'thumb_dim': THUMB_POSTER_DIM, + 'art_dim': ART_AR16X9_DIM + }, + 'show': { + 'fallback': 'show', + 'thumb_dim': THUMB_POSTER_DIM, + 'art_dim': ART_AR16X9_DIM + }, + 'collection': { + 'fallback': 'movie', + 'thumb_dim': THUMB_POSTER_DIM, + 'art_dim': ART_AR16X9_DIM + }, + 'album': { + 'fallback': 'music', + 'thumb_dim': THUMB_SQUARE_DIM + }, + 'artist': { + 'fallback': 'music', + 'thumb_dim': THUMB_SQUARE_DIM + }, + 'track': { + 'fallback': 'music', + 'thumb_dim': THUMB_SQUARE_DIM + }, + 'photo': { + 'fallback': 'photo', + 'thumb_dim': THUMB_SQUARE_DIM + }, + 'clip': { + 'fallback': 'movie16x9', + 'thumb_dim': THUMB_POSTER_DIM + }, +} + +TYPE_PLURAL = { + 'artist': T(32347, 'artists'), + 'album': T(32461, 'albums'), + 'movie': T(32348, 'Movies'), + 'photo': T(32349, 'photos'), + 'show': T(32350, 'Shows'), + 'episode': T(32458, 'Episodes'), + 'collection': T(32490, 'Collections'), + 'folder': T(32491, 'Folders'), +} + +SORT_KEYS = { + 'movie': { + 'titleSort': {'title': T(32357, 'By Name'), 'display': T(32358, 'Name'), 'defSortDesc': False}, + 'addedAt': {'title': T(32351, 'By Date Added'), 'display': T(32352, 'Date Added'), 'defSortDesc': True}, + 'originallyAvailableAt': {'title': T(32353, 'By Release Date'), 'display': T(32354, 'Release Date'), + 'defSortDesc': True}, + 'lastViewedAt': {'title': T(32355, 'By Date Viewed'), 'display': T(32356, 'Date Viewed'), 'defSortDesc': True}, + 'rating': {'title': T(33107, 'By Critic Rating'), 'display': T(33108, ' Critic Rating'), 'defSortDesc': True}, + 'audienceRating': {'title': T(33101, 'By Audience Rating'), 'display': T(33102, 'Audience Rating'), + 'defSortDesc': True}, + # called "Rating" in PlexWeb, using more obvious "This is this user's rating" here + 'userRating': {'title': T(33103, 'By my Rating'), 'display': T(33104, 'My Rating'), 'defSortDesc': True}, + 'contentRating': {'title': T(33105, 'By Content Rating'), 'display': T(33106, 'Content Rating'), + 'defSortDesc': True}, + 'resolution': {'title': T(32361, 'By Resolution'), 'display': T(32362, 'Resolution'), 'defSortDesc': True}, + 'duration': {'title': T(32363, 'By Duration'), 'display': T(32364, 'Duration'), 'defSortDesc': True}, + 'unwatched': {'title': T(32367, 'By Unplayed'), 'display': T(32368, 'Unplayed'), 'defSortDesc': False}, + 'viewCount': {'title': T(32371, 'By Play Count'), 'display': T(32372, 'Play Count'), 'defSortDesc': True} + }, + 'show': { + 'titleSort': {'title': T(32357, 'By Name'), 'display': T(32358, 'Name'), 'defSortDesc': False}, + 'show.titleSort': {'title': T(32457, 'By Show'), 'display': T(32456, 'Show'), 'defSortDesc': False}, + 'originallyAvailableAt': {'title': T(32365, 'By First Aired'), 'display': T(32366, 'First Aired'), + 'defSortDesc': False}, + 'unviewedLeafCount': {'title': T(32367, 'By Unplayed'), 'display': T(32368, 'Unplayed'), 'defSortDesc': True}, + 'rating': {'title': T(33107, 'By Critic Rating'), 'display': T(33108, ' Critic Rating'), 'defSortDesc': True}, + 'audienceRating': {'title': T(33101, 'By Audience Rating'), 'display': T(33102, 'Audience Rating'), + 'defSortDesc': True}, + # called "Rating" in PlexWeb, using more obvious "This is this user's rating" here + 'userRating': {'title': T(33103, 'By my Rating'), 'display': T(33104, 'My Rating'), 'defSortDesc': True}, + 'contentRating': {'title': T(33105, 'By Content Rating'), 'display': T(33106, 'Content Rating'), + 'defSortDesc': True}, + }, + 'artist': { + 'titleSort': {'title': T(32357, 'By Name'), 'display': T(32358, 'Name'), 'defSortDesc': False}, + 'artist.titleSort': {'title': T(32463, 'By Artist'), 'display': T(32462, 'Artist'), 'defSortDesc': False}, + 'lastViewedAt': {'title': T(32369, 'By Date Played'), 'display': T(32370, 'Date Played'), 'defSortDesc': False}, + }, + 'photo': { + 'titleSort': {'title': T(32357, 'By Name'), 'display': T(32358, 'Name'), 'defSortDesc': False}, + 'originallyAvailableAt': {'title': T(32373, 'By Date Taken'), 'display': T(32374, 'Date Taken'), + 'defSortDesc': True} + }, + 'photodirectory': {}, + 'collection': {} +} + +ITEM_TYPE = None + + +def setItemType(type_=None): + assert type_ is not None, "Invalid type: None" + global ITEM_TYPE + ITEM_TYPE = type_ + util.setGlobalProperty('item.type', str(ITEM_TYPE)) + + +class ChunkRequestTask(backgroundthread.Task): + def setup(self, section, start, size, callback, filter_=None, sort=None, unwatched=False, subDir=False): + self.section = section + self.start = start + self.size = size + self.callback = callback + self.filter = filter_ + self.sort = sort + self.unwatched = unwatched + self.subDir = subDir + return self + + def contains(self, pos): + return self.start <= pos <= (self.start + self.size) + + def run(self): + if self.isCanceled(): + return + + try: + type_ = None + if ITEM_TYPE == 'episode': + type_ = 4 + elif ITEM_TYPE == 'album': + type_ = 9 + elif ITEM_TYPE == 'collection': + type_ = 18 + + if ITEM_TYPE == 'folder': + items = self.section.folder(self.start, self.size, self.subDir) + else: + items = self.section.all(self.start, self.size, self.filter, self.sort, self.unwatched, type_=type_) + + if self.isCanceled(): + return + self.callback(items, self.start) + except plexnet.exceptions.BadRequest: + util.DEBUG_LOG('404 on section: {0}'.format(repr(self.section.title))) + + +class PhotoPropertiesTask(backgroundthread.Task): + def setup(self, photo, callback): + self.photo = photo + self.callback = callback + return self + + def run(self): + if self.isCanceled(): + return + + try: + self.photo.reload() + self.callback(self.photo) + except plexnet.exceptions.BadRequest: + util.DEBUG_LOG('404 on photo reload: {0}'.format(self.photo)) + + +class LibrarySettings(object): + def __init__(self, section_or_server_id, ignoreLibrarySettings=False): + self.ignoreLibrarySettings = ignoreLibrarySettings + if isinstance(section_or_server_id, six.string_types): + self.serverID = section_or_server_id + self.sectionID = None + else: + self.serverID = section_or_server_id.getServer().uuid + self.sectionID = section_or_server_id.key + + self._loadSettings() + + def _loadSettings(self): + if self.ignoreLibrarySettings: + self._settings = {} + return + + if not self.sectionID: + return + + jsonString = util.getSetting('library.settings.{0}'.format(self.serverID), '') + self._settings = {} + try: + self._settings = json.loads(jsonString) + except ValueError: + pass + except: + util.ERROR() + + setItemType(self.getItemType() or ITEM_TYPE) + + def getItemType(self): + if not self._settings or self.sectionID not in self._settings: + return None + + return self._settings[self.sectionID].get('ITEM_TYPE') + + def setItemType(self, item_type): + setItemType(item_type) + + if self.sectionID not in self._settings: + self._settings[self.sectionID] = {} + + self._settings[self.sectionID]['ITEM_TYPE'] = item_type + + self._saveSettings() + + def _saveSettings(self): + jsonString = json.dumps(self._settings) + util.setSetting('library.settings.{0}'.format(self.serverID), jsonString) + + def setSection(self, section_id): + self.sectionID = section_id + + def getSetting(self, setting, default=None): + if not self._settings or self.sectionID not in self._settings: + return default + + if ITEM_TYPE not in self._settings[self.sectionID]: + return default + + return self._settings[self.sectionID][ITEM_TYPE].get(setting, default) + + def setSetting(self, setting, value): + if self.sectionID not in self._settings: + self._settings[self.sectionID] = {} + + if ITEM_TYPE not in self._settings[self.sectionID]: + self._settings[self.sectionID][ITEM_TYPE] = {} + + self._settings[self.sectionID][ITEM_TYPE][setting] = value + + self._saveSettings() + + +class ChunkedWrapList(kodigui.ManagedControlList): + LIST_MAX = CHUNK_SIZE * 3 + + def __getitem__(self, idx): + # if isinstance(idx, slice): + # return self.items[idx] + # else: + idx = idx % self.LIST_MAX + return self.items[idx] + # return self.getListItem(idx) + + +class ChunkModeWrapped(object): + ALL_MAX = CHUNK_SIZE * 2 + + def __init__(self): + self.reset() + + def reset(self): + self.midStart = 0 + self.itemCount = 0 + self.keys = {} + + def addKeyRange(self, key, krange): + self.keys[key] = krange + + def getKey(self, pos): + for k, krange in self.keys.items(): + if krange[0] <= pos <= krange[1]: + return k + + def isAtBeginning(self): + return self.midStart == 0 + + def posIsForward(self, pos): + if self.itemCount <= self.ALL_MAX: + return False + return pos >= self.midStart + CHUNK_SIZE + + def posIsBackward(self, pos): + if self.itemCount <= self.ALL_MAX: + return False + return pos < self.midStart + + def posIsValid(self, pos): + return self.midStart - CHUNK_SIZE <= pos < self.midStart + (CHUNK_SIZE * 2) + + def shift(self, mod): + if mod < 0 and self.midStart == 0: + return None + elif mod > 0 and self.midStart + CHUNK_SIZE >= self.itemCount: + return None + + offset = CHUNK_SIZE * mod + self.midStart += offset + start = self.midStart + offset + + return start + + def shiftToKey(self, key, keyStart=None): + if keyStart is None: + if key not in self.keys: + util.DEBUG_LOG('CHUNK MODE: NO ITEMS FOR KEY') + return + + keyStart = self.keys[key][0] + self.midStart = keyStart - keyStart % CHUNK_SIZE + return keyStart, max(self.midStart - CHUNK_SIZE, 0) + + def addObjects(self, pos, objects): + if not self.posIsValid(pos): + return + + if pos == self.midStart - CHUNK_SIZE: + self.objects = objects + self.objects[CHUNK_SIZE:] + elif pos == self.midStart: + self.objects = self.objects[:CHUNK_SIZE] + objects + self.objects[CHUNK_SIZE * 2:] + elif pos == self.midStart + CHUNK_SIZE: + self.objects = self.objects[:CHUNK_SIZE * 2] + objects + + +class CustomScrollBar(object): + def __init__(self, window, bar_group_id, bar_image_id, bar_image_focus_id, button_id, min_bar_height=20): + self._barGroup = window.getControl(bar_group_id) + self._barImage = window.getControl(bar_image_id) + self._barImageFocus = window.getControl(bar_image_focus_id) + self._button = window.getControl(button_id) + self.height = self._button.getHeight() + self.x, self.y = self._barGroup.getPosition() + self._minBarHeight = min_bar_height + self._barHeight = min_bar_height + self.reset() + + def reset(self): + self.size = 0 + self.count = 0 + self.pos = 0 + + def setSizeAndCount(self, size, count): + self.size = size + self.count = count + self._barHeight = min(self.height, max(self._minBarHeight, int(self.height * (count / float(size))))) + self._moveHeight = self.height - self._barHeight + self._barImage.setHeight(self._barHeight) + self._barImageFocus.setHeight(self._barHeight) + self.setPosition(0) + + def setPosition(self, pos): + self.pos = pos + offset = int((pos / float(max(self.size, 2) - 1)) * self._moveHeight) + self._barGroup.setPosition(self.x, self.y + offset) + + def getPosFromY(self, y): + y -= int(self._barHeight / 2) + 150 + y = min(max(y, 0), self._moveHeight) + return int((self.size - 1) * (y / float(self._moveHeight))) + + def onMouseDrag(self, window, action): + y = window.mouseYTrans(action.getAmount2()) + y -= int(self._barHeight / 2) + 150 + y = min(max(y, 0), self._moveHeight) + self._barGroup.setPosition(self.x, self.y) + + +class LibraryWindow(kodigui.MultiWindow, windowutils.UtilMixin): + bgXML = 'script-plex-blank.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + + def __init__(self, *args, **kwargs): + kodigui.MultiWindow.__init__(self, *args, **kwargs) + windowutils.UtilMixin.__init__(self) + self.section = kwargs.get('section') + self.filter = kwargs.get('filter_') + self.subDir = kwargs.get('subDir') + self.keyItems = {} + self.firstOfKeyItems = {} + self.tasks = backgroundthread.Tasks() + self.backgroundSet = False + self.showPanelControl = None + self.keyListControl = None + self.lastItem = None + self.lastFocusID = None + self.lastNonOptionsFocusID = None + self.refill = False + + self.dcpjPos = 0 + self.dcpjThread = None + self.dcpjTimeout = 0 + + self.dragging = False + + self.cleared = True + self.librarySettings = LibrarySettings(self.section, + ignoreLibrarySettings=kwargs.get("ignoreLibrarySettings", False)) + self.reset() + + self.lock = threading.Lock() + + def reset(self): + util.setGlobalProperty('sort', '') + self.filterUnwatched = self.librarySettings.getSetting('filter.unwatched', False) + self.sort = self.librarySettings.getSetting('sort', 'titleSort') + self.sortDesc = self.librarySettings.getSetting('sort.desc', False) + + self.chunkMode = None + #if ITEM_TYPE in ('episode', 'album'): + # self.chunkMode = ChunkModeWrapped() + + key = self.section.key + if not key.isdigit(): + key = self.section.getLibrarySectionId() + viewtype = util.getSetting('viewtype.{0}.{1}'.format(self.section.server.uuid, key)) + + if self.chunkMode: + if self.section.TYPE in ('artist', 'photo', 'photodirectory'): + self.setWindows(VIEWS_SQUARE_CHUNKED.get('all')) + self.setDefault(VIEWS_SQUARE_CHUNKED.get(viewtype)) + else: + self.setWindows(VIEWS_POSTER_CHUNKED.get('all')) + self.setDefault(VIEWS_POSTER_CHUNKED.get(viewtype)) + else: + if self.section.TYPE in ('artist', 'photo', 'photodirectory'): + self.setWindows(VIEWS_SQUARE.get('all')) + self.setDefault(VIEWS_SQUARE.get(viewtype)) + else: + self.setWindows(VIEWS_POSTER.get('all')) + self.setDefault(VIEWS_POSTER.get(viewtype)) + + @busy.dialog() + def doClose(self): + self.tasks.kill() + kodigui.MultiWindow.doClose(self) + + def onFirstInit(self): + self.scrollBar = None + #if ITEM_TYPE in ('episode', 'album'): + # self.scrollBar = CustomScrollBar(self, 950, 952, 953, 951) + + if self.showPanelControl and not self.refill: + self.showPanelControl.newControl(self) + self.keyListControl.newControl(self) + self.showPanelControl.selectItem(0) + self.setFocusId(self.VIEWTYPE_BUTTON_ID) + self.setBoolProperty("initialized", True) + else: + if self.chunkMode: + self.showPanelControl = ChunkedWrapList(self, self.POSTERS_PANEL_ID, 5) + else: + self.showPanelControl = kodigui.ManagedControlList(self, self.POSTERS_PANEL_ID, 5) + + hideFilterOptions = self.section.TYPE == 'photodirectory' or self.section.TYPE == 'collection' + + self.keyListControl = kodigui.ManagedControlList(self, self.KEY_LIST_ID, 27) + self.setProperty('subDir', self.subDir and '1' or '') + self.setProperty('no.options', self.section.TYPE != 'photodirectory' and '1' or '') + self.setProperty('unwatched.hascount', self.section.TYPE == 'show' and '1' or '') + util.setGlobalProperty('sort', self.sort) + self.setProperty('filter1.display', self.filterUnwatched and T(32368, 'UNPLAYED') or T(32345, 'All')) + self.setProperty('sort.display', SORT_KEYS[self.section.TYPE].get(self.sort, SORT_KEYS['movie'].get(self.sort))['title']) + self.setProperty('media.itemType', ITEM_TYPE or self.section.TYPE) + self.setProperty('media.type', TYPE_PLURAL.get(ITEM_TYPE or self.section.TYPE, self.section.TYPE)) + self.setProperty('media', self.section.TYPE) + self.setProperty('hide.filteroptions', hideFilterOptions and '1' or '') + + self.setTitle() + self.setBoolProperty("initialized", True) + self.fill() + self.refill = False + if self.getProperty('no.content') or self.getProperty('no.content.filtered'): + self.setFocusId(self.HOME_BUTTON_ID) + else: + self.setFocusId(self.POSTERS_PANEL_ID) + + def onAction(self, action): + try: + if self.dragging: + if not action == xbmcgui.ACTION_MOUSE_DRAG: + self.dragging = False + self.setBoolProperty('dragging', self.dragging) + + if action.getId() in MOVE_SET: + if util.advancedSettings.dynamicBackgrounds: + mli = self.showPanelControl.getSelectedItem() + if mli and mli.dataSource: + self.updateBackgroundFrom(mli.dataSource) + + controlID = self.getFocusId() + if controlID == self.POSTERS_PANEL_ID or controlID == self.SCROLLBAR_ID: + self.updateKey() + self.checkChunkedNav(action) + elif controlID == self.CUSTOM_SCOLLBAR_BUTTON_ID: + if action == xbmcgui.ACTION_MOVE_UP: + self.shiftSelection(-12) + elif action == xbmcgui.ACTION_MOVE_DOWN: + self.shiftSelection(12) + # elif action == xbmcgui.KEY_MOUSE_DRAG_START: + # self.onMouseDragStart(action) + # elif action == xbmcgui.KEY_MOUSE_DRAG_END: + # self.onMouseDragEnd(action) + elif action == xbmcgui.ACTION_MOUSE_DRAG: + self.onMouseDrag(action) + elif action == xbmcgui.ACTION_CONTEXT_MENU: + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + self.lastNonOptionsFocusID = self.lastFocusID + self.setFocusId(self.OPTIONS_GROUP_ID) + return + else: + if self.lastNonOptionsFocusID: + self.setFocusId(self.lastNonOptionsFocusID) + self.lastNonOptionsFocusID = None + return + + elif action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)) and \ + (not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): + if xbmc.getCondVisibility('Integer.IsGreater(Container(101).ListItem.Property(index),5)'): + self.showPanelControl.selectItem(0) + return + + self.updateItem() + + except: + util.ERROR() + + kodigui.MultiWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.POSTERS_PANEL_ID: + self.showPanelClicked() + elif controlID == self.KEY_LIST_ID: + self.keyClicked() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.PLAY_BUTTON_ID: + self.playButtonClicked() + elif controlID == self.SHUFFLE_BUTTON_ID: + self.shuffleButtonClicked() + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked() + elif controlID == self.VIEWTYPE_BUTTON_ID: + self.viewTypeButtonClicked() + elif controlID == self.SORT_BUTTON_ID: + self.sortButtonClicked() + elif controlID == self.FILTER1_BUTTON_ID: + self.filter1ButtonClicked() + elif controlID == self.ITEM_TYPE_BUTTON_ID: + self.itemTypeButtonClicked() + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def onFocus(self, controlID): + self.lastFocusID = controlID + + if controlID == self.KEY_LIST_ID: + self.selectKey() + + if player.PLAYER.bgmPlaying: + player.PLAYER.stopAndWait() + + def onItemChanged(self, mli): + if not mli: + return + + if not mli.dataSource or not mli.dataSource.TYPE == 'photo': + return + + self.showPhotoItemProperties(mli.dataSource) + + def onMouseDragStart(self, action): + if not self.scrollBar: + return + + controlID = self.getFocusId() + if controlID != self.CUSTOM_SCOLLBAR_BUTTON_ID: + return + + self.dragging = True + self.setBoolProperty('dragging', self.dragging) + + def onMouseDragEnd(self, action): + if not self.scrollBar: + return + + if not self.dragging: + return + + self.dragging = False + self.setBoolProperty('dragging', self.dragging) + + y = self.mouseYTrans(action.getAmount2()) + + pos = self.scrollBar.getPosFromY(y) + self.shiftSelection(pos=pos) + + def onMouseDrag(self, action): + if not self.scrollBar: + return + + if not self.dragging: + controlID = self.getFocusId() + if controlID != self.CUSTOM_SCOLLBAR_BUTTON_ID: + return + + self.onMouseDragStart(action) + if not self.dragging: + return + + # self.scrollBar.onMouseDrag(self, action) + + y = self.mouseYTrans(action.getAmount2()) + + pos = self.scrollBar.getPosFromY(y) + if self.chunkMode.posIsForward(pos) or self.chunkMode.posIsBackward(pos): + self.shiftSelection(pos=pos) + else: + self.showPanelControl.selectItem(pos) + self.checkChunkedNav() + + def shiftSelection(self, offset=0, pos=None): + if pos is not None: + self.scrollBar.setPosition(pos) + return self.delayedChunkedPosJump(pos) + else: + mli = self.showPanelControl.getSelectedItem() + + try: + idx = int(mli.getProperty('index')) + except ValueError: + return + + target = idx + offset + if target >= self.chunkMode.itemCount: + pos = self.chunkMode.itemCount - 1 + elif target < 0: + pos = 0 + else: + pos = self.showPanelControl.getSelectedPosition() + pos += offset + + if pos < 0 or pos >= self.showPanelControl.size(): + pos = pos % self.showPanelControl.size() + + self.showPanelControl.selectItem(pos) + self.checkChunkedNav(idx=pos) + + def updateKey(self, mli=None): + mli = mli or self.showPanelControl.getSelectedItem() + if not mli: + return + + if self.lastItem != mli: + self.lastItem = mli + self.onItemChanged(mli) + + util.setGlobalProperty('key', mli.getProperty('key')) + + self.selectKey(mli) + + def checkChunkedNav(self, action=None, idx=None): + if not self.chunkMode: + return + + # if action == xbmcgui.ACTION_PAGE_DOWN: + # idx = self.showPanelControl.getSelectedPosition() - 5 + # if idx < 0: + # idx += self.showPanelControl.size() + # mli = self.showPanelControl.getListItem(idx) + # self.showPanelControl.selectItem(idx) + # elif action == xbmcgui.ACTION_PAGE_UP: + # idx = self.showPanelControl.getSelectedPosition() + 5 + # if idx >= self.showPanelControl.size(): + # idx %= self.showPanelControl.size() + # mli = self.showPanelControl.getListItem(idx) + # self.showPanelControl.selectItem(idx) + # else: + mli = self.showPanelControl.getSelectedItem() + + try: + if idx is not None: + pos = int(self.showPanelControl[idx].getProperty('index')) + else: + pos = int(mli.getProperty('index')) + + if pos >= self.chunkMode.itemCount: + raise ValueError + except ValueError: + if self.chunkMode.isAtBeginning() and action not in (xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_PAGE_DOWN): + idx = 0 + else: + idx = ((self.chunkMode.itemCount - 1) % self.showPanelControl.LIST_MAX) + + self.showPanelControl.selectItem(idx) + mli = self.showPanelControl[idx] + self.updateKey(mli) + if self.scrollBar: + try: + pos = int(mli.getProperty('index')) + self.scrollBar.setPosition(pos) + except ValueError: + pass + + if idx == 0 and action == xbmcgui.ACTION_MOVE_UP: + self.setFocusId(600) + + return + + if self.scrollBar: + self.scrollBar.setPosition(pos) + + if self.chunkMode.posIsForward(pos): + self.shiftChunks() + elif self.chunkMode.posIsBackward(pos): + self.shiftChunks(-1) + + def shiftChunks(self, mod=1): + start = self.chunkMode.shift(mod) + if start is None: + return + + if start < 0: + self.chunkCallback([None] * CHUNK_SIZE, -CHUNK_SIZE) + else: + self.chunkCallback([False] * CHUNK_SIZE, start) + task = ChunkRequestTask().setup( + self.section, start, CHUNK_SIZE, self.chunkCallback, filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, subDir=self.subDir + ) + + self.tasks.add(task) + backgroundthread.BGThreader.addTasksToFront([task]) + + def selectKey(self, mli=None): + if not mli: + mli = self.showPanelControl.getSelectedItem() + if not mli: + return + + li = self.keyItems.get(mli.getProperty('key')) + if not li: + return + self.keyListControl.selectItem(li.pos()) + + def searchButtonClicked(self): + self.processCommand(search.dialog(self, section_id=self.section.key)) + + def delayedChunkedPosJump(self, pos): + if not self.cleared: + self.chunkCallback(None, None, clear=True) + self.dcpjTimeout = time.time() + 0.5 + self.dcpjPos = pos + if not self.dcpjThread or not self.dcpjThread.is_alive(): + self.dcpjThread = threading.Thread(target=self._chunkedPosJump) + self.dcpjThread.start() + + def _chunkedPosJump(self): + while not util.MONITOR.waitForAbort(0.1): + if time.time() >= self.dcpjTimeout: + break + else: + return + + keyStart_start = self.chunkMode.shiftToKey(None, keyStart=self.dcpjPos) + if not keyStart_start: + return + + keyStart, start = keyStart_start + pos = keyStart % self.showPanelControl.LIST_MAX + self.chunkedPosJump(pos, start) + self.showPanelControl.selectItem(pos) + + def chunkedPosJump(self, pos, start=None): + if start is None: + start = max(pos - CHUNK_SIZE, 0) + + mul = 3 + if not start: + mul = 2 + + tasks = [] + for x in range(mul): + task = ChunkRequestTask().setup( + self.section, + start + (CHUNK_SIZE * x), + CHUNK_SIZE, + self.chunkCallback, + filter_=self.getFilterOpts(), + sort=self.getSortOpts(), + unwatched=self.filterUnwatched, + subDir=self.subDir + ) + + self.tasks.add(task) + tasks.append(task) + + mid = tasks.pop(1) + backgroundthread.BGThreader.addTasksToFront([mid] + tasks) + + def keyClicked(self): + li = self.keyListControl.getSelectedItem() + if not li: + return + + if self.chunkMode: + keyStart_start = self.chunkMode.shiftToKey(li.dataSource) + if not keyStart_start: + return + keyStart, start = keyStart_start + + pos = keyStart % self.showPanelControl.LIST_MAX + self.chunkedPosJump(pos, start) + else: + mli = self.firstOfKeyItems.get(li.dataSource) + if not mli: + return + pos = mli.pos() + + self.showPanelControl.selectItem(pos) + self.setFocusId(self.POSTERS_PANEL_ID) + util.setGlobalProperty('key', li.dataSource) + + def playButtonClicked(self, shuffle=False): + filter_ = self.getFilterOpts() + sort = self.getSortOpts() + args = {} + if filter_: + args[filter_[0]] = filter_[1] + + if sort: + args['sort'] = '{0}:{1}'.format(*sort) + + if self.section.TYPE == 'movie': + args['sourceType'] = '1' + elif self.section.TYPE == 'show': + args['sourceType'] = '2' + elif self.section.TYPE != 'collection': + args['sourceType'] = '8' + + # When the list is filtered by unwatched, play and shuffle button should only play unwatched videos + if self.filterUnwatched: + args['unwatched'] = '1' + + pq = playqueue.createPlayQueueForItem(self.section, options={'shuffle': shuffle}, args=args) + opener.open(pq) + + def shuffleButtonClicked(self): + self.playButtonClicked(shuffle=True) + + def optionsButtonClicked(self): + options = [] + if xbmc.getCondVisibility('Player.HasAudio + MusicPlayer.HasNext'): + options.append({'key': 'play_next', 'display': T(32325, 'Play Next')}) + + # if self.section.TYPE not in ('artist', 'photo', 'photodirectory'): + # options.append({'key': 'mark_watched', 'display': 'Mark All Watched'}) + # options.append({'key': 'mark_unwatched', 'display': 'Mark All Unwatched'}) + + # if xbmc.getCondVisibility('Player.HasAudio') and self.section.TYPE == 'artist': + # options.append({'key': 'add_to_queue', 'display': 'Add To Queue'}) + + # if False: + # options.append({'key': 'add_to_playlist', 'display': 'Add To Playlist'}) + + if self.section.TYPE == 'photodirectory': + if options: + options.append(dropdown.SEPARATOR) + options.append({'key': 'to_section', 'display': T(32324, u'Go to {0}').format(self.section.getLibrarySectionTitle())}) + + choice = dropdown.showDropdown(options, (255, 205)) + if not choice: + return + + if choice['key'] == 'play_next': + xbmc.executebuiltin('PlayerControl(Next)') + elif choice['key'] == 'to_section': + self.goHome(self.section.getLibrarySectionId()) + + def itemTypeButtonClicked(self): + options = [] + + if self.section.TYPE == 'show': + for t in ('show', 'episode', 'collection'): + options.append({'type': t, 'display': TYPE_PLURAL.get(t, t)}) + elif self.section.TYPE == 'movie': + for t in ('movie', 'collection', 'folder'): + options.append({'type': t, 'display': TYPE_PLURAL.get(t, t)}) + elif self.section.TYPE == 'artist': + for t in ('artist', 'album', 'collection'): + options.append({'type': t, 'display': TYPE_PLURAL.get(t, t)}) + else: + return + + result = dropdown.showDropdown(options, (1280, 106), with_indicator=True) + if not result: + return + + choice = result['type'] + + if choice == ITEM_TYPE: + return + + self.tasks.cancel() + + self.showPanelControl = None # TODO: Need to do some check here I think + + self.librarySettings.setItemType(choice) + + self.reset() + + self.clearFilters() + self.resetSort() + + if not self.nextWindow(False): + self.setProperty('media.type', TYPE_PLURAL.get(ITEM_TYPE or self.section.TYPE, self.section.TYPE)) + self.setProperty('sort.display', SORT_KEYS[self.section.TYPE].get(self.sort, SORT_KEYS['movie'].get(self.sort))['title']) + self.fill() + + def sortButtonClicked(self): + desc = 'script.plex/indicators/arrow-down.png' + asc = 'script.plex/indicators/arrow-up.png' + ind = self.sortDesc and desc or asc + + options = [] + defSortByOption = {} + + if self.section.TYPE == 'movie': + searchTypes = ['titleSort', 'addedAt', 'originallyAvailableAt', 'lastViewedAt', 'rating', 'audienceRating', + 'userRating', 'contentRating', 'resolution', 'duration'] + if ITEM_TYPE == 'collection': + searchTypes = ['titleSort', 'addedAt', 'contentRating'] + + for stype in searchTypes: + option = SORT_KEYS['movie'].get(stype).copy() + option['type'] = stype + option['indicator'] = self.sort == stype and ind or '' + defSortByOption[stype] = option.get('defSortDesc') + options.append(option) + elif self.section.TYPE == 'show': + searchTypes = ['titleSort', 'addedAt', 'lastViewedAt', 'originallyAvailableAt', 'rating', + 'audienceRating', 'userRating', 'contentRating', 'unviewedLeafCount'] + if ITEM_TYPE == 'episode': + searchTypes = ['titleSort', 'show.titleSort', 'addedAt', 'originallyAvailableAt', 'lastViewedAt', 'rating', + 'audienceRating', 'userRating'] + elif ITEM_TYPE == 'collection': + searchTypes = ['titleSort', 'addedAt'] + + for stype in searchTypes: + option = SORT_KEYS['show'].get(stype, SORT_KEYS['movie'].get(stype)).copy() + option['type'] = stype + option['indicator'] = self.sort == stype and ind or '' + defSortByOption[stype] = option.get('defSortDesc') + options.append(option) + elif self.section.TYPE == 'artist': + searchTypes = ['titleSort', 'addedAt', 'lastViewedAt', 'viewCount'] + if ITEM_TYPE == 'album': + searchTypes = ['titleSort', 'artist.titleSort', 'addedAt', 'lastViewedAt', 'viewCount', 'originallyAvailableAt', 'rating'] + elif ITEM_TYPE == 'collection': + searchTypes = ['titleSort', 'addedAt'] + + for stype in searchTypes: + option = SORT_KEYS['artist'].get(stype, SORT_KEYS['movie'].get(stype)).copy() + option['type'] = stype + option['indicator'] = self.sort == stype and ind or '' + defSortByOption[stype] = option.get('defSortDesc') + options.append(option) + elif self.section.TYPE == 'photo': + searchTypes = ['titleSort', 'addedAt', 'originallyAvailableAt', 'rating'] + for stype in searchTypes: + option = SORT_KEYS['photo'].get(stype, SORT_KEYS['movie'].get(stype)).copy() + option['type'] = stype + option['indicator'] = self.sort == stype and ind or '' + defSortByOption[stype] = option.get('defSortDesc') + options.append(option) + else: + return + + result = dropdown.showDropdown(options, (1280, 106), with_indicator=True) + if not result: + return + + choice = result['type'] + + forceRefresh = False + if choice == self.sort: + self.sortDesc = not self.sortDesc + else: + self.sortDesc = defSortByOption.get(choice, False) + if choice == 'titleSort': + forceRefresh = True + + self.sort = choice + + self.librarySettings.setSetting('sort', self.sort) + self.librarySettings.setSetting('sort.desc', self.sortDesc) + + util.setGlobalProperty('sort', choice) + self.setProperty('sort.display', result['title']) + + self.sortShowPanel(choice, forceRefresh) + + def viewTypeButtonClicked(self): + for task in self.tasks: + if task.isValid(): + task.cancel() + self.refill = True + + with self.lock: + self.showPanelControl.invalidate() + win = self.nextWindow() + + key = self.section.key + if not key.isdigit(): + key = self.section.getLibrarySectionId() + util.setSetting('viewtype.{0}.{1}'.format(self.section.server.uuid, key), win.VIEWTYPE) + + def sortShowPanel(self, choice, force_refresh=False): + if force_refresh or self.chunkMode or self.showPanelControl.size() == 0: + self.fillShows() + return + + if choice == 'addedAt': + self.showPanelControl.sort(lambda i: i.dataSource.addedAt, reverse=self.sortDesc) + elif choice == 'originallyAvailableAt': + self.showPanelControl.sort(lambda i: i.dataSource.get('originallyAvailableAt'), reverse=self.sortDesc) + elif choice == 'lastViewedAt': + self.showPanelControl.sort(lambda i: i.dataSource.get('lastViewedAt'), reverse=self.sortDesc) + elif choice == 'viewCount': + self.showPanelControl.sort(lambda i: i.dataSource.get('titleSort') or i.dataSource.title) + self.showPanelControl.sort(lambda i: i.dataSource.get('viewCount').asInt(), reverse=self.sortDesc) + elif choice == 'titleSort': + self.showPanelControl.sort(lambda i: i.dataSource.get('titleSort') or i.dataSource.title, reverse=self.sortDesc) + self.keyListControl.sort(lambda i: i.getProperty('original'), reverse=self.sortDesc) + elif choice == 'show.titleSort': + self.showPanelControl.sort(lambda i: i.label, reverse=self.sortDesc) + self.keyListControl.sort(lambda i: i.getProperty('original'), reverse=self.sortDesc) + elif choice == 'artist.titleSort': + self.showPanelControl.sort(lambda i: i.label, reverse=self.sortDesc) + self.keyListControl.sort(lambda i: i.getProperty('original'), reverse=self.sortDesc) + elif choice == 'rating': + self.showPanelControl.sort(lambda i: i.dataSource.get('titleSort') or i.dataSource.title) + self.showPanelControl.sort(lambda i: i.dataSource.get('rating').asFloat(), reverse=self.sortDesc) + elif choice == 'audienceRating': + self.showPanelControl.sort(lambda i: i.dataSource.get('titleSort') or i.dataSource.title) + self.showPanelControl.sort(lambda i: i.dataSource.get('audienceRating').asFloat(), reverse=self.sortDesc) + elif choice == 'userRating': + self.showPanelControl.sort(lambda i: i.dataSource.get('titleSort') or i.dataSource.title) + self.showPanelControl.sort(lambda i: i.dataSource.get('userRating').asFloat(), reverse=self.sortDesc) + elif choice == 'contentRating': + self.showPanelControl.sort(lambda i: i.dataSource.get('titleSort') or i.dataSource.title) + self.showPanelControl.sort(lambda i: i.dataSource.get('contentRating'), reverse=self.sortDesc) + elif choice == 'resolution': + self.showPanelControl.sort(lambda i: i.dataSource.maxHeight, reverse=self.sortDesc) + elif choice == 'duration': + self.showPanelControl.sort(lambda i: i.dataSource.duration.asInt(), reverse=self.sortDesc) + elif choice == 'unviewedLeafCount': + self.showPanelControl.sort(lambda i: i.dataSource.unViewedLeafCount, reverse=self.sortDesc) + + self.showPanelControl.selectItem(0) + self.setFocusId(self.POSTERS_PANEL_ID) + self.backgroundSet = False + self.setBackground([item.dataSource for item in self.showPanelControl], 0, randomize=not util.advancedSettings.dynamicBackgrounds) + + def subOptionCallback(self, option): + check = 'script.plex/home/device/check.png' + options = None + subKey = None + if self.filter: + if self.filter.get('sub'): + subKey = self.filter['sub']['val'] + + if option['type'] in ( + 'year', 'decade', 'genre', 'contentRating', 'collection', 'director', 'actor', 'country', 'studio', 'resolution', 'labels', + 'make', 'model', 'aperture', 'exposure', 'iso', 'lens' + ): + options = [{'val': o.key, 'display': o.title, 'indicator': o.key == subKey and check or ''} for o in self.section.listChoices(option['type'])] + if not options: + options = [{'val': None, 'display': T(32375, 'No filters available'), 'ignore': True}] + + return options + + def hasFilter(self, ftype): + if not self.filter: + return False + + return self.filter['type'] == ftype + + def filter1ButtonClicked(self): + check = 'script.plex/home/device/check.png' + + options = [] + + if self.section.TYPE in ('movie', 'show'): + options.append({'type': 'unwatched', 'display': T(32368, 'UNPLAYED').upper(), 'indicator': self.filterUnwatched and check or ''}) + + if self.filter: + options.append({'type': 'clear_filter', 'display': T(32376, 'CLEAR FILTER').upper(), 'indicator': 'script.plex/indicators/remove.png'}) + + if options: + options.append(None) # Separator + + optionsMap = { + 'year': {'type': 'year', 'display': T(32377, 'Year'), 'indicator': self.hasFilter('year') and check or ''}, + 'decade': {'type': 'decade', 'display': T(32378, 'Decade'), 'indicator': self.hasFilter('decade') and check or ''}, + 'genre': {'type': 'genre', 'display': T(32379, 'Genre'), 'indicator': self.hasFilter('genre') and check or ''}, + 'contentRating': {'type': 'contentRating', 'display': T(32380, 'Content Rating'), 'indicator': self.hasFilter('contentRating') and check or ''}, + 'network': {'type': 'studio', 'display': T(32381, 'Network'), 'indicator': self.hasFilter('studio') and check or ''}, + 'collection': {'type': 'collection', 'display': T(32382, 'Collection'), 'indicator': self.hasFilter('collection') and check or ''}, + 'director': {'type': 'director', 'display': T(32383, 'Director'), 'indicator': self.hasFilter('director') and check or ''}, + 'actor': {'type': 'actor', 'display': T(32384, 'Actor'), 'indicator': self.hasFilter('actor') and check or ''}, + 'country': {'type': 'country', 'display': T(32385, 'Country'), 'indicator': self.hasFilter('country') and check or ''}, + 'studio': {'type': 'studio', 'display': T(32386, 'Studio'), 'indicator': self.hasFilter('studio') and check or ''}, + 'resolution': {'type': 'resolution', 'display': T(32362, 'Resolution'), 'indicator': self.hasFilter('resolution') and check or ''}, + 'labels': {'type': 'labels', 'display': T(32387, 'Labels'), 'indicator': self.hasFilter('labels') and check or ''}, + + 'make': {'type': 'make', 'display': T(32388, 'Camera Make'), 'indicator': self.hasFilter('make') and check or ''}, + 'model': {'type': 'model', 'display': T(32389, 'Camera Model'), 'indicator': self.hasFilter('model') and check or ''}, + 'aperture': {'type': 'aperture', 'display': T(32390, 'Aperture'), 'indicator': self.hasFilter('aperture') and check or ''}, + 'exposure': {'type': 'exposure', 'display': T(32391, 'Shutter Speed'), 'indicator': self.hasFilter('exposure') and check or ''}, + 'iso': {'type': 'iso', 'display': 'ISO', 'indicator': self.hasFilter('iso') and check or ''}, + 'lens': {'type': 'lens', 'display': T(32392, 'Lens'), 'indicator': self.hasFilter('lens') and check or ''} + } + + if self.section.TYPE == 'movie': + for k in ('year', 'decade', 'genre', 'contentRating', 'collection', 'director', 'actor', 'country', 'studio', 'resolution', 'labels'): + options.append(optionsMap[k]) + elif self.section.TYPE == 'show': + if ITEM_TYPE == 'episode': + for k in ('year', 'collection', 'resolution'): + options.append(optionsMap[k]) + elif ITEM_TYPE == 'album': + for k in ('genre', 'year', 'decade', 'collection', 'labels'): + options.append(optionsMap[k]) + else: + for k in ('year', 'genre', 'contentRating', 'network', 'collection', 'actor', 'labels'): + options.append(optionsMap[k]) + elif self.section.TYPE == 'artist': + for k in ('genre', 'country', 'collection'): + options.append(optionsMap[k]) + elif self.section.TYPE == 'photo': + for k in ('year', 'make', 'model', 'aperture', 'exposure', 'iso', 'lens', 'labels'): + options.append(optionsMap[k]) + + result = dropdown.showDropdown(options, (980, 106), with_indicator=True, suboption_callback=self.subOptionCallback) + if not result: + return + + choice = result['type'] + + if choice == 'clear_filter': + self.filter = None + elif choice == 'unwatched': + self.filterUnwatched = not self.filterUnwatched + self.librarySettings.setSetting('filter.unwatched', self.filterUnwatched) + else: + self.filter = result + + self.updateFilterDisplay() + + if self.filter or choice in ('clear_filter', 'unwatched'): + self.fill() + + def clearFilters(self): + self.filter = None + self.filterUnwatched = False + self.librarySettings.setSetting('filter.unwatched', self.filterUnwatched) + self.updateFilterDisplay() + + def resetSort(self): + self.sort = 'titleSort' + self.sortDesc = False + + self.librarySettings.setSetting('sort', self.sort) + self.librarySettings.setSetting('sort.desc', self.sortDesc) + + util.setGlobalProperty('sort', self.sort) + self.setProperty('sort.display', SORT_KEYS[self.section.TYPE].get(self.sort, SORT_KEYS['movie'].get(self.sort))['title']) + + def updateFilterDisplay(self): + if self.filter: + disp = self.filter['display'] + if self.filter.get('sub'): + disp = u'{0}: {1}'.format(disp, self.filter['sub']['display']) + self.setProperty('filter1.display', disp) + self.setProperty('filter2.display', self.filterUnwatched and T(32368, 'Unplayed') or '') + else: + self.setProperty('filter2.display', '') + self.setProperty('filter1.display', self.filterUnwatched and T(32368, 'Unplayed') or T(32345, 'All')) + + def showPanelClicked(self): + mli = self.showPanelControl.getSelectedItem() + if not mli or not mli.dataSource: + return + + sectionType = self.section.TYPE + + updateUnwatchedAndProgress = False + + if mli.dataSource.TYPE == 'collection': + prevItemType = self.librarySettings.getItemType() + self.processCommand(opener.open(mli.dataSource)) + self.librarySettings.setItemType(prevItemType) + elif self.section.TYPE == 'show' or mli.dataSource.TYPE == 'show' or mli.dataSource.TYPE == 'season' or mli.dataSource.TYPE == 'episode': + if ITEM_TYPE == 'episode' or mli.dataSource.TYPE == 'episode' or mli.dataSource.TYPE == 'season': + self.openItem(mli.dataSource) + else: + self.processCommand(opener.handleOpen(subitems.ShowWindow, media_item=mli.dataSource, parent_list=self.showPanelControl)) + if mli.dataSource.TYPE != 'season': # NOTE: A collection with Seasons doesn't have the leafCount/viewedLeafCount until you actually go into the season so we can't update the unwatched count here + updateUnwatchedAndProgress = True + elif self.section.TYPE == 'movie' or mli.dataSource.TYPE == 'movie': + datasource = mli.dataSource + if datasource.isDirectory(): + cls = self.section.__class__ + section = cls(self.section.data, self.section.initpath, self.section.server, self.section.container) + sectionId = section.key + if not sectionId.isdigit(): + sectionId = section.getLibrarySectionId() + + section.set('librarySectionID', sectionId) + section.key = datasource.key + section.title = datasource.title + + self.processCommand(opener.handleOpen(LibraryWindow, windows=self._windows, default_window=self._next, section=section, filter_=self.filter, subDir=True)) + self.librarySettings.setItemType(self.librarySettings.getItemType()) + else: + self.processCommand(opener.handleOpen(preplay.PrePlayWindow, video=datasource, parent_list=self.showPanelControl)) + updateUnwatchedAndProgress = True + elif self.section.TYPE == 'artist' or mli.dataSource.TYPE == 'artist' or mli.dataSource.TYPE == 'album' or mli.dataSource.TYPE == 'track': + if ITEM_TYPE == 'album' or mli.dataSource.TYPE == 'album' or mli.dataSource.TYPE == 'track': + self.openItem(mli.dataSource) + else: + self.processCommand(opener.handleOpen(subitems.ArtistWindow, media_item=mli.dataSource, parent_list=self.showPanelControl)) + elif self.section.TYPE in ('photo', 'photodirectory'): + self.showPhoto(mli.dataSource) + + if not mli: + return + + if mli.dataSource and not mli.dataSource.exists(): + self.showPanelControl.removeItem(mli.pos()) + return + + if updateUnwatchedAndProgress: + self.updateUnwatchedAndProgress(mli) + + def showPhoto(self, photo): + if isinstance(photo, plexnet.photo.Photo) or photo.TYPE == 'clip': + self.processCommand(opener.open(photo)) + else: + self.processCommand(opener.sectionClicked(photo)) + + def updateUnwatchedAndProgress(self, mli): + mli.dataSource.reload() + if mli.dataSource.isWatched: + mli.setProperty('unwatched', '') + mli.setProperty('unwatched.count', '') + else: + if self.section.TYPE == 'show' or mli.dataSource.TYPE == 'show' or mli.dataSource.TYPE == 'season': + mli.setProperty('unwatched.count', str(mli.dataSource.unViewedLeafCount)) + else: + mli.setProperty('unwatched', '1') + mli.setProperty('progress', util.getProgressImage(mli.dataSource)) + + def setTitle(self): + if self.section.TYPE == 'artist': + self.setProperty('screen.title', T(32394, 'MUSIC').upper()) + elif self.section.TYPE in ('photo', 'photodirectory'): + self.setProperty('screen.title', T(32349, 'photos').upper()) + elif self.section.TYPE == 'collection': + self.setProperty('screen.title', T(32382, 'COLLECTION').upper()) + else: + self.setProperty('screen.title', self.section.TYPE == 'show' and T(32393, 'TV SHOWS').upper() or T(32348, 'movies').upper()) + + self.updateFilterDisplay() + + def updateItem(self, mli=None): + mli = mli or self.showPanelControl.getSelectedItem() + if not mli or mli.dataSource: + return + + for task in self.tasks: + if task.contains(mli.pos()): + util.DEBUG_LOG('Moving task to front: {0}'.format(task)) + backgroundthread.BGThreader.moveToFront(task) + break + + def setBackground(self, items, position, randomize=True): + if self.backgroundSet: + return + + if randomize: + item = random.choice(items) + self.updateBackgroundFrom(item) + else: + # we want the first item of the first chunk + if position != 0: + return + + self.updateBackgroundFrom(items[0]) + self.backgroundSet = True + + def fill(self): + if self.chunkMode: + self.chunkMode.reset() + + self.backgroundSet = False + + if self.section.TYPE in ('photo', 'photodirectory'): + self.fillPhotos() + else: + self.fillShows() + + def getFilterOpts(self): + if not self.filter: + return None + + if not self.filter.get('sub'): + util.DEBUG_LOG('Filter missing sub-filter data') + return None + + return (self.filter['type'], six.moves.urllib.parse.unquote_plus(self.filter['sub']['val'])) + + def getSortOpts(self): + if not self.sort: + return None + + return (self.sort, self.sortDesc and 'desc' or 'asc') + + @busy.dialog() + def fillShows(self): + self.setBoolProperty('no.content', False) + self.setBoolProperty('no.content.filtered', False) + items = [] + jitems = [] + self.keyItems = {} + self.firstOfKeyItems = {} + totalSize = 0 + + type_ = None + if ITEM_TYPE == 'episode': + type_ = 4 + elif ITEM_TYPE == 'album': + type_ = 9 + elif ITEM_TYPE == 'collection': + type_ = 18 + + idx = 0 + fallback = 'script.plex/thumb_fallbacks/{0}.png'.format(TYPE_KEYS.get(self.section.type, TYPE_KEYS['movie'])['fallback']) + + if self.sort != 'titleSort' or ITEM_TYPE == 'folder' or self.subDir or self.section.TYPE == "collection": + if ITEM_TYPE == 'folder': + sectionAll = self.section.folder(0, 0, self.subDir) + else: + sectionAll = self.section.all(0, 0, filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, type_=type_) + + totalSize = sectionAll.totalSize.asInt() + + if not totalSize: + self.showPanelControl.reset() + self.keyListControl.reset() + + if self.filter or self.filterUnwatched: + self.setBoolProperty('no.content.filtered', True) + else: + self.setBoolProperty('no.content', True) + else: + if not self.chunkMode: + for x in range(totalSize): + mli = kodigui.ManagedListItem('') + mli.setProperty('thumb.fallback', fallback) + mli.setProperty('index', str(x)) + items.append(mli) + else: + jumpList = self.section.jumpList(filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, type_=type_) + + if not jumpList: + self.showPanelControl.reset() + self.keyListControl.reset() + + if self.filter or self.filterUnwatched: + self.setBoolProperty('no.content.filtered', True) + else: + self.setBoolProperty('no.content', True) + + if jumpList is None: + util.messageDialog("Error", "There was an error.") + + return + + for kidx, ji in enumerate(jumpList): + mli = kodigui.ManagedListItem(ji.title, data_source=ji.key) + mli.setProperty('key', ji.key) + mli.setProperty('original', '{0:02d}'.format(kidx)) + self.keyItems[ji.key] = mli + jitems.append(mli) + totalSize += ji.size.asInt() + + if self.chunkMode: + self.chunkMode.addKeyRange(ji.key, (idx, (idx + ji.size.asInt()) - 1)) + idx += ji.size.asInt() + else: + for x in range(ji.size.asInt()): + mli = kodigui.ManagedListItem('') + mli.setProperty('key', ji.key) + mli.setProperty('thumb.fallback', fallback) + mli.setProperty('index', str(idx)) + items.append(mli) + if not x: # i.e. first item + self.firstOfKeyItems[ji.key] = mli + idx += 1 + + util.setGlobalProperty('key', jumpList[0].key) + + if self.scrollBar: + self.scrollBar.setSizeAndCount(totalSize, 12) + + if self.chunkMode: + self.chunkMode.itemCount = totalSize + items = [ + kodigui.ManagedListItem('', properties={'index': str(i)}) for i in range(CHUNK_SIZE * 2) + ] + [ + kodigui.ManagedListItem('') for i in range(CHUNK_SIZE) + ] + + self.showPanelControl.reset() + self.keyListControl.reset() + + self.showPanelControl.addItems(items) + self.keyListControl.addItems(jitems) + + self.showPanelControl.selectItem(0) + self.setFocusId(self.POSTERS_PANEL_ID) + + tasks = [] + ct = 0 + for start in range(0, totalSize, CHUNK_SIZE): + tasks.append( + ChunkRequestTask().setup( + self.section, start, CHUNK_SIZE, self.chunkCallback, filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, subDir=self.subDir + ) + ) + ct += 1 + + if self.chunkMode and ct > 1: + break + + self.tasks.add(tasks) + backgroundthread.BGThreader.addTasksToFront(tasks) + + def showPhotoItemProperties(self, photo): + if photo.isFullObject(): + return + + task = PhotoPropertiesTask().setup(photo, self._showPhotoItemProperties) + self.tasks.add(task) + backgroundthread.BGThreader.addTasksToFront([task]) + + def _showPhotoItemProperties(self, photo): + mli = self.showPanelControl.getSelectedItem() + if not mli or not mli.dataSource.TYPE == 'photo': + for mli in self.showPanelControl: + if mli.dataSource == photo: + break + else: + return + + mli.setProperty('camera.model', photo.media[0].model) + mli.setProperty('camera.lens', photo.media[0].lens) + + attrib = [] + if photo.media[0].height: + attrib.append(u'{0} x {1}'.format(photo.media[0].width, photo.media[0].height)) + + orientation = photo.media[0].parts[0].orientation + if orientation: + attrib.append(u'{0} Mo'.format(orientation)) + + container = photo.media[0].container_ or os.path.splitext(photo.media[0].parts[0].file)[-1][1:].lower() + if container == 'jpg': + container = 'jpeg' + attrib.append(container.upper()) + if attrib: + mli.setProperty('photo.dims', u' \u2022 '.join(attrib)) + + settings = [] + if photo.media[0].iso: + settings.append('ISO {0}'.format(photo.media[0].iso)) + if photo.media[0].aperture: + settings.append('{0}'.format(photo.media[0].aperture)) + if photo.media[0].exposure: + settings.append('{0}'.format(photo.media[0].exposure)) + mli.setProperty('camera.settings', u' \u2022 '.join(settings)) + mli.setProperty('photo.summary', photo.get('summary')) + + @busy.dialog() + def fillPhotos(self): + self.setBoolProperty('no.content', False) + self.setBoolProperty('no.content.filtered', False) + items = [] + keys = [] + self.firstOfKeyItems = {} + idx = 0 + + if self.section.TYPE == 'photodirectory': + photos = self.section.all() + else: + photos = self.section.all(filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched) + + if not photos: + return + + photo = random.choice(photos) + self.updateBackgroundFrom(photo) + thumbDim = TYPE_KEYS.get(self.section.type, TYPE_KEYS['movie'])['thumb_dim'] + fallback = 'script.plex/thumb_fallbacks/{0}.png'.format(TYPE_KEYS.get(self.section.type, TYPE_KEYS['movie'])['fallback']) + + if not photos: + if self.filter or self.filterUnwatched: + self.setBoolProperty('no.content.filtered', True) + else: + self.setBoolProperty('no.content', True) + return + + for photo in photos: + title = photo.title + if photo.TYPE == 'photodirectory': + thumb = photo.composite.asTranscodedImageURL(*thumbDim) + mli = kodigui.ManagedListItem(title, thumbnailImage=thumb, data_source=photo) + mli.setProperty('is.folder', '1') + else: + thumb = photo.defaultThumb.asTranscodedImageURL(*thumbDim) + label2 = util.cleanLeadingZeros(photo.originallyAvailableAt.asDatetime('%d %B %Y')) + mli = kodigui.ManagedListItem(title, label2, thumbnailImage=thumb, data_source=photo) + + mli.setProperty('thumb.fallback', fallback) + mli.setProperty('index', str(idx)) + + key = title[0].upper() + if key not in KEYS: + key = '#' + if key not in keys: + self.firstOfKeyItems[key] = mli + keys.append(key) + mli.setProperty('key', str(key)) + items.append(mli) + idx += 1 + + litems = [] + self.keyItems = {} + for i, key in enumerate(keys): + mli = kodigui.ManagedListItem(key, data_source=key) + mli.setProperty('key', key) + mli.setProperty('original', '{0:02d}'.format(i)) + self.keyItems[key] = mli + litems.append(mli) + + self.showPanelControl.reset() + self.keyListControl.reset() + + self.showPanelControl.addItems(items) + self.keyListControl.addItems(litems) + + if keys: + util.setGlobalProperty('key', keys[0]) + + def chunkCallback(self, items, start, clear=False): + if clear: + with self.lock: + items = [kodigui.ManagedListItem('') for i in range(CHUNK_SIZE * 3)] + + self.showPanelControl.reset() + self.showPanelControl.addItems(items) + + self.cleared = True + return + + if self.cleared: + self.cleared = False + busy.widthDialog(self._chunkCallback, self, items, start) + else: + self._chunkCallback(items, start) + + def _chunkCallback(self, items, start): + if self.chunkMode and not self.chunkMode.posIsValid(start): + return + + with self.lock: + if self.chunkMode and not self.chunkMode.posIsValid(start): + return + pos = start + self.setBackground(items, pos, randomize=not util.advancedSettings.dynamicBackgrounds) + + thumbDim = TYPE_KEYS.get(self.section.type, TYPE_KEYS['movie'])['thumb_dim'] + artDim = TYPE_KEYS.get(self.section.type, TYPE_KEYS['movie']).get('art_dim', (256, 256)) + + showUnwatched = False + if (self.section.TYPE in ('movie', 'show') and items[0].TYPE != 'collection') or (self.section.TYPE == 'collection' and items[0].TYPE in ('movie', 'show', 'episode')): # NOTE: A collection with Seasons doesn't have the leafCount/viewedLeafCount until you actually go into the season so we can't update the unwatched count here + showUnwatched = True + + if self.chunkMode and len(items) < CHUNK_SIZE: + items += [None] * (CHUNK_SIZE - len(items)) + + if ITEM_TYPE == 'episode': + for offset, obj in enumerate(items): + mli = self.showPanelControl[pos] + if obj: + mli.dataSource = obj + mli.setProperty('index', str(pos)) + if obj.index: + subtitle = u'{0}{1} \u2022 {2}{3}'.format(T(32310, 'S'), obj.parentIndex, T(32311, 'E'), obj.index) + mli.setProperty('subtitle', subtitle) + subtitle = "\n" + subtitle + else: + subtitle = ' - ' + obj.originallyAvailableAt.asDatetime('%m/%d/%y') + mli.setLabel((obj.defaultTitle or '') + subtitle) + + mli.setThumbnailImage(obj.defaultThumb.asTranscodedImageURL(*thumbDim)) + + mli.setProperty('summary', obj.summary) + + # # mli.setProperty('key', self.chunkMode.getKey(pos)) + + mli.setLabel2(util.durationToText(obj.fixedDuration())) + mli.setProperty('art', obj.defaultArt.asTranscodedImageURL(*artDim)) + if not obj.isWatched: + mli.setProperty('unwatched', '1') + else: + mli.clear() + if obj is False: + mli.setProperty('index', str(pos)) + else: + mli.setProperty('index', '') + + pos += 1 + + elif ITEM_TYPE == 'album': + for offset, obj in enumerate(items): + mli = self.showPanelControl[pos] + if obj: + mli.dataSource = obj + mli.setProperty('index', str(pos)) + mli.setLabel(u'{0}\n{1}'.format(obj.parentTitle, obj.title)) + + mli.setThumbnailImage(obj.defaultThumb.asTranscodedImageURL(*thumbDim)) + + mli.setProperty('summary', obj.summary) + + # # mli.setProperty('key', self.chunkMode.getKey(pos)) + + mli.setLabel2(obj.year) + + if self.chunkMode: + mli.setProperty('key', self.chunkMode.getKey(pos)) + + else: + mli.clear() + if obj is False: + mli.setProperty('index', str(pos)) + else: + mli.setProperty('index', '') + + pos += 1 + else: + for offset, obj in enumerate(items): + mli = self.showPanelControl[pos] + if obj: + mli.setProperty('index', str(pos)) + mli.setLabel(obj.defaultTitle or '') + mli.setThumbnailImage(obj.defaultThumb.asTranscodedImageURL(*thumbDim)) + mli.dataSource = obj + mli.setProperty('summary', obj.get('summary')) + + #if self.chunkMode: + # mli.setProperty('key', self.chunkMode.getKey(pos)) + #else: + # mli.setProperty('key', obj.key) + + if showUnwatched and obj.TYPE != 'collection': + if not obj.isDirectory(): + mli.setLabel2(util.durationToText(obj.fixedDuration())) + mli.setProperty('art', obj.defaultArt.asTranscodedImageURL(*artDim)) + if not obj.isWatched and obj.TYPE != "Directory": + if self.section.TYPE == 'show' or obj.TYPE == 'show' or obj.TYPE == 'season': + mli.setProperty('unwatched.count', str(obj.unViewedLeafCount)) + else: + mli.setProperty('unwatched', '1') + + mli.setProperty('progress', util.getProgressImage(obj)) + else: + mli.clear() + if obj is False: + mli.setProperty('index', str(pos)) + else: + mli.setProperty('index', '') + + pos += 1 + + +class PostersWindow(kodigui.ControlledWindow): + xmlFile = 'script-plex-posters.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + POSTERS_PANEL_ID = 101 + KEY_LIST_ID = 151 + SCROLLBAR_ID = 152 + + OPTIONS_GROUP_ID = 200 + + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + PLAYER_STATUS_BUTTON_ID = 204 + + SORT_BUTTON_ID = 210 + FILTER1_BUTTON_ID = 211 + FILTER2_BUTTON_ID = 212 + ITEM_TYPE_BUTTON_ID = 312 + + PLAY_BUTTON_ID = 301 + SHUFFLE_BUTTON_ID = 302 + OPTIONS_BUTTON_ID = 303 + VIEWTYPE_BUTTON_ID = 304 + + VIEWTYPE = 'panel' + MULTI_WINDOW_ID = 0 + + CUSTOM_SCOLLBAR_BUTTON_ID = 951 + + +class PostersSmallWindow(PostersWindow): + xmlFile = 'script-plex-posters-small.xml' + VIEWTYPE = 'panel2' + MULTI_WINDOW_ID = 1 + + +class PostersChunkedWindow(PostersWindow): + xmlFile = 'script-plex-listview-16x9-chunked.xml' + VIEWTYPE = 'list' + MULTI_WINDOW_ID = 0 + + +class ListView16x9Window(PostersWindow): + xmlFile = 'script-plex-listview-16x9.xml' + VIEWTYPE = 'list' + MULTI_WINDOW_ID = 2 + + +class ListView16x9ChunkedWindow(PostersWindow): + xmlFile = 'script-plex-listview-16x9-chunked.xml' + VIEWTYPE = 'list' + MULTI_WINDOW_ID = 1 + + +class SquaresWindow(PostersWindow): + xmlFile = 'script-plex-squares.xml' + VIEWTYPE = 'panel' + MULTI_WINDOW_ID = 0 + + +class SquaresChunkedWindow(PostersWindow): + xmlFile = 'script-plex-listview-square-chunked.xml' + VIEWTYPE = 'list' + MULTI_WINDOW_ID = 0 + + +class ListViewSquareWindow(PostersWindow): + xmlFile = 'script-plex-listview-square.xml' + VIEWTYPE = 'list' + MULTI_WINDOW_ID = 1 + + +class ListViewSquareChunkedWindow(PostersWindow): + xmlFile = 'script-plex-listview-square-chunked.xml' + VIEWTYPE = 'list' + MULTI_WINDOW_ID = 1 + + +VIEWS_POSTER = { + 'panel': PostersWindow, + 'panel2': PostersSmallWindow, + 'list': ListView16x9Window, + 'all': (PostersWindow, PostersSmallWindow, ListView16x9Window) +} + +VIEWS_POSTER_CHUNKED = { + 'panel': PostersChunkedWindow, + 'list': ListView16x9ChunkedWindow, + 'all': (PostersChunkedWindow, ListView16x9ChunkedWindow) +} + +VIEWS_SQUARE = { + 'panel': SquaresWindow, + 'list': ListViewSquareWindow, + 'all': (SquaresWindow, ListViewSquareWindow) +} + +VIEWS_SQUARE_CHUNKED = { + 'panel': SquaresChunkedWindow, + 'list': ListViewSquareChunkedWindow, + 'all': (SquaresChunkedWindow, ListViewSquareChunkedWindow) +} \ No newline at end of file diff --git a/script.plexmod/lib/windows/mixins.py b/script.plexmod/lib/windows/mixins.py new file mode 100644 index 000000000..1e3586847 --- /dev/null +++ b/script.plexmod/lib/windows/mixins.py @@ -0,0 +1,61 @@ +# coding=utf-8 + +from lib import util + +from . import kodigui + + +class SeasonsMixin(): + SEASONS_CONTROL_ATTR = "subItemListControl" + + THUMB_DIMS = { + 'show': { + 'main.thumb': util.scaleResolution(347, 518), + 'item.thumb': util.scaleResolution(174, 260) + }, + 'episode': { + 'main.thumb': util.scaleResolution(347, 518), + 'item.thumb': util.scaleResolution(198, 295) + }, + 'artist': { + 'main.thumb': util.scaleResolution(519, 519), + 'item.thumb': util.scaleResolution(215, 215) + } + } + + def _createListItem(self, mediaItem, obj): + mli = kodigui.ManagedListItem( + obj.title or '', + thumbnailImage=obj.defaultThumb.asTranscodedImageURL(*self.THUMB_DIMS[mediaItem.type]['item.thumb']), + data_source=obj + ) + return mli + + def fillSeasons(self, mediaItem, update=False, seasonsFilter=None, selectSeason=None): + seasons = mediaItem.seasons() + if not seasons or (seasonsFilter and not seasonsFilter(seasons)): + return False + + items = [] + idx = 0 + for season in seasons: + if selectSeason and season == selectSeason: + continue + + mli = self._createListItem(mediaItem, season) + if mli: + mli.setProperty('index', str(idx)) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/show.png') + mli.setProperty('unwatched.count', not season.isWatched and str(season.unViewedLeafCount) or '') + items.append(mli) + idx += 1 + + subItemListControl = getattr(self, self.SEASONS_CONTROL_ATTR) + if update: + subItemListControl.replaceItems(items) + else: + subItemListControl.reset() + subItemListControl.addItems(items) + + return True + diff --git a/script.plexmod/lib/windows/musicplayer.py b/script.plexmod/lib/windows/musicplayer.py new file mode 100644 index 000000000..d2277eb5e --- /dev/null +++ b/script.plexmod/lib/windows/musicplayer.py @@ -0,0 +1,169 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui +from . import currentplaylist +from . import opener + +from lib import player +from lib import util + + +def timeDisplay(ms): + h = ms / 3600000 + m = (ms % 3600000) / 60000 + s = (ms % 60000) / 1000 + return '{0:0>2}:{1:0>2}:{2:0>2}'.format(h, m, s) + + +def simplifiedTimeDisplay(ms): + left, right = timeDisplay(ms).rsplit(':', 1) + left = left.lstrip('0:') or '0' + return left + ':' + right + + +class MusicPlayerWindow(currentplaylist.CurrentPlaylistWindow): + xmlFile = 'script-plex-music_player.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + SEEK_BUTTON_ID = 100 + SEEK_IMAGE_ID = 200 + SHUFFLE_REMOTE_BUTTON_ID = 422 + REPEAT_BUTTON_ID = 401 + SKIP_PREV_BUTTON_ID = 404 + SKIP_NEXT_BUTTON_ID = 409 + STOP_BUTTON_ID = 407 + + SEEK_IMAGE_WIDTH = 1920 + + BAR_RIGHT = 1920 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.track = kwargs.get('track') + self.playlist = kwargs.get('playlist') + self.album = kwargs.get('album') + self.selectedOffset = 0 + self.exitCommand = None + + if self.track: + self.duration = self.track.duration.asInt() + else: + self.setDuration() + + def onFirstInit(self): + if self.playlist and self.playlist.isRemote: + self.playlist.on('change', self.updateProperties) + self.setupSeekbar() + self.selectionBoxMax = self.SEEK_IMAGE_WIDTH - (self.selectionBoxHalf - 3) + + self.updateProperties() + self.play() + self.setFocusId(406) + + def doClose(self, **kwargs): + player.PLAYER.off('playback.started', self.onPlayBackStarted) + if self.playlist and self.playlist.isRemote: + self.playlist.off('change', self.updateProperties) + kodigui.ControlledWindow.doClose(self) + + def onAction(self, action): + try: + if action == xbmcgui.ACTION_STOP: + self.stopButtonClicked() + return + except: + util.ERROR() + + super().onAction(action) + + def onClick(self, controlID): + if controlID == self.PLAYLIST_BUTTON_ID: + self.showPlaylist() + elif controlID == self.SEEK_BUTTON_ID: + self.seekButtonClicked() + elif controlID == self.SHUFFLE_REMOTE_BUTTON_ID: + self.playlist.setShuffle() + elif controlID == self.REPEAT_BUTTON_ID: + self.repeatButtonClicked() + elif controlID == self.SKIP_PREV_BUTTON_ID: + self.skipPrevButtonClicked() + elif controlID == self.SKIP_NEXT_BUTTON_ID: + self.skipNextButtonClicked() + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked((1240, 1060)) + elif controlID == self.STOP_BUTTON_ID: + self.stopButtonClicked() + + def repeatButtonClicked(self): + if self.playlist and self.playlist.isRemote: + if xbmc.getCondVisibility('Playlist.IsRepeatOne'): + xbmc.executebuiltin('PlayerControl(RepeatOff)') + elif self.playlist.isRepeat: + self.playlist.setRepeat(False) + self.playlist.refresh(force=True) + xbmc.executebuiltin('PlayerControl(RepeatOne)') + else: + self.playlist.setRepeat(True) + self.playlist.refresh(force=True) + else: + xbmc.executebuiltin('PlayerControl(Repeat)') + + def skipPrevButtonClicked(self): + if not xbmc.getCondVisibility('MusicPlayer.HasPrevious') and self.playlist and self.playlist.isRemote: + util.DEBUG_LOG('MusicPlayer: No previous in Kodi playlist - refreshing remote PQ') + if not self.playlist.refresh(force=True, wait=True): + return + + xbmc.executebuiltin('PlayerControl(Previous)') + + def skipNextButtonClicked(self): + if not xbmc.getCondVisibility('MusicPlayer.HasNext') and self.playlist and self.playlist.isRemote: + util.DEBUG_LOG('MusicPlayer: No next in Kodi playlist - refreshing remote PQ') + if not self.playlist.refresh(force=True, wait=True): + return + + xbmc.executebuiltin('PlayerControl(Next)') + + def showPlaylist(self): + self.processCommand(opener.handleOpen(currentplaylist.CurrentPlaylistWindow, winID=xbmcgui.getCurrentWindowId())) + + def stopButtonClicked(self): + self.doClose() + + def updateProperties(self, **kwargs): + if self.playlist: + if self.playlist.isRemote: + self.setProperty('pq.isRemote', '1') + self.setProperty('pq.hasnext', self.playlist.allowSkipNext and '1' or '') + self.setProperty('pq.hasprev', self.playlist.allowSkipPrev and '1' or '') + self.setProperty('pq.repeat', self.playlist.isRepeat and '1' or '') + self.setProperty('pq.shuffled', self.playlist.isShuffled and '1' or '') + else: + self.setProperties(('pq.isRemote', 'pq.hasnext', 'pq.hasprev', 'pq.repeat', 'pq.shuffled'), '') + + def play(self): + if not self.track: + return + + if util.trackIsPlaying(self.track): + return + + fanart = None + if self.playlist: + fanart = self.playlist.get('composite') or self.playlist.defaultArt + # player.PLAYER.playAudio(self.track, fanart=self.getProperty('background')) + if self.album: + index = 0 + for i, track in enumerate(self.album.tracks()): + if track == self.track: + index = i + player.PLAYER.playAlbum(self.album, startpos=index, fanart=fanart) + elif self.playlist: + player.PLAYER.playAudioPlaylist(self.playlist, startpos=list(self.playlist.items()).index(self.track), fanart=fanart) + else: + player.PLAYER.playAudio(self.track) diff --git a/script.plexmod/lib/windows/opener.py b/script.plexmod/lib/windows/opener.py new file mode 100644 index 000000000..4aed06b2d --- /dev/null +++ b/script.plexmod/lib/windows/opener.py @@ -0,0 +1,169 @@ +from __future__ import absolute_import +from . import busy + +from plexnet import playqueue, plexapp, plexlibrary +from lib import util +import six + + +def open(obj, **kwargs): + if isinstance(obj, playqueue.PlayQueue): + if busy.widthDialog(obj.waitForInitialization, None): + if obj.type == 'audio': + from . import musicplayer + return handleOpen(musicplayer.MusicPlayerWindow, track=obj.current(), playlist=obj) + elif obj.type == 'photo': + from . import photos + return handleOpen(photos.PhotoWindow, play_queue=obj) + else: + from . import videoplayer + videoplayer.play(play_queue=obj) + return '' + elif isinstance(obj, six.string_types): + key = obj + if not obj.startswith('/'): + key = '/library/metadata/{0}'.format(obj) + return open(plexapp.SERVERMANAGER.selectedServer.getObject(key), **kwargs) + elif obj.TYPE == 'episode': + return episodeClicked(obj, **kwargs) + elif obj.TYPE == 'movie': + return playableClicked(obj, **kwargs) + elif obj.TYPE in ('show'): + return showClicked(obj, **kwargs) + elif obj.TYPE in ('artist'): + return artistClicked(obj, **kwargs) + elif obj.TYPE in ('season'): + return seasonClicked(obj, **kwargs) + elif obj.TYPE in ('album'): + return albumClicked(obj, **kwargs) + elif obj.TYPE in ('photo',): + return photoClicked(obj, **kwargs) + elif obj.TYPE in ('photodirectory'): + return photoDirectoryClicked(obj, **kwargs) + elif obj.TYPE in ('track'): + return trackClicked(obj, **kwargs) + elif obj.TYPE in ('playlist'): + return playlistClicked(obj, **kwargs) + elif obj.TYPE in ('clip'): + from . import videoplayer + return videoplayer.play(video=obj) + elif obj.TYPE in ('collection'): + return collectionClicked(obj, **kwargs) + elif obj.TYPE in ('Genre'): + return genreClicked(obj, **kwargs) + elif obj.TYPE in ('Director'): + return directorClicked(obj, **kwargs) + elif obj.TYPE in ('Role'): + return actorClicked(obj, **kwargs) + + +def handleOpen(winclass, **kwargs): + w = None + try: + autoPlay = kwargs.pop("auto_play", False) + if autoPlay and hasattr(winclass, "doAutoPlay"): + w = winclass.create(show=False, **kwargs) + if w.doAutoPlay(): + w.modal() + else: + w = winclass.open(**kwargs) + return w.exitCommand or '' + except AttributeError: + pass + except util.NoDataException: + raise + finally: + del w + util.garbageCollect() + + return '' + + +def playableClicked(playable, **kwargs): + from . import preplay + return handleOpen(preplay.PrePlayWindow, video=playable, **kwargs) + + +def episodeClicked(episode, **kwargs): + from . import episodes + return handleOpen(episodes.EpisodesWindow, episode=episode, **kwargs) + + +def showClicked(show, **kwargs): + from . import subitems + return handleOpen(subitems.ShowWindow, media_item=show, **kwargs) + + +def artistClicked(artist, **kwargs): + from . import subitems + return handleOpen(subitems.ArtistWindow, media_item=artist, **kwargs) + + +def seasonClicked(season, **kwargs): + from . import episodes + return handleOpen(episodes.EpisodesWindow, season=season, **kwargs) + + +def albumClicked(album, **kwargs): + from . import tracks + return handleOpen(tracks.AlbumWindow, album=album, **kwargs) + + +def photoClicked(photo, **kwargs): + from . import photos + return handleOpen(photos.PhotoWindow, photo=photo, **kwargs) + + +def trackClicked(track, **kwargs): + from . import musicplayer + return handleOpen(musicplayer.MusicPlayerWindow, track=track, **kwargs) + + +def photoDirectoryClicked(photodirectory, **kwargs): + return sectionClicked(photodirectory, **kwargs) + + +def playlistClicked(pl, **kwargs): + from . import playlist + return handleOpen(playlist.PlaylistWindow, playlist=pl, **kwargs) + + +def collectionClicked(collection, **kwargs): + return sectionClicked(collection, **kwargs) + + +def sectionClicked(section, filter_=None, **kwargs): + from . import library + library.ITEM_TYPE = section.TYPE + key = section.key + if not key.isdigit(): + key = section.getLibrarySectionId() + viewtype = util.getSetting('viewtype.{0}.{1}'.format(section.server.uuid, key)) + if section.TYPE in ('artist', 'photo', 'photodirectory'): + default = library.VIEWS_SQUARE.get(viewtype) + return handleOpen( + library.LibraryWindow, windows=library.VIEWS_SQUARE.get('all'), default_window=default, section=section, filter_=filter_, **kwargs + ) + else: + default = library.VIEWS_POSTER.get(viewtype) + return handleOpen( + library.LibraryWindow, windows=library.VIEWS_POSTER.get('all'), default_window=default, section=section, filter_=filter_, **kwargs + ) + + +def genreClicked(genre, **kwargs): + section = plexlibrary.LibrarySection.fromFilter(genre) + filter_ = {'type': genre.FILTER, 'display': 'Genre', 'sub': {'val': genre.id, 'display': genre.tag}} + return sectionClicked(section, filter_, **kwargs) + + +def directorClicked(director, **kwargs): + section = plexlibrary.LibrarySection.fromFilter(director) + filter_ = {'type': director.FILTER, 'display': 'Director', 'sub': {'val': director.id, 'display': director.tag}} + return sectionClicked(section, filter_, **kwargs) + + +def actorClicked(actor, **kwargs): + section = plexlibrary.LibrarySection.fromFilter(actor) + filter_ = {'type': actor.FILTER, 'display': 'Actor', 'sub': {'val': actor.id, 'display': actor.tag}} + return sectionClicked(section, filter_, ignoreLibrarySettings=True, **kwargs) diff --git a/script.plexmod/lib/windows/optionsdialog.py b/script.plexmod/lib/windows/optionsdialog.py new file mode 100644 index 000000000..dbbd0961e --- /dev/null +++ b/script.plexmod/lib/windows/optionsdialog.py @@ -0,0 +1,55 @@ +from __future__ import absolute_import +from . import kodigui + +from lib import util + + +class OptionsDialog(kodigui.BaseDialog): + xmlFile = 'script-plex-options_dialog.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + GROUP_ID = 100 + BUTTON_IDS = (1001, 1002, 1003) + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + self.header = kwargs.get('header') + self.info = kwargs.get('info') + self.button0 = kwargs.get('button0') + self.button1 = kwargs.get('button1') + self.button2 = kwargs.get('button2') + self.buttonChoice = None + + def onFirstInit(self): + self.setProperty('header', self.header) + self.setProperty('info', self.info) + + if self.button2: + self.setProperty('button.2', self.button2) + + if self.button1: + self.setProperty('button.1', self.button1) + + if self.button0: + self.setProperty('button.0', self.button0) + + self.setBoolProperty('initialized', True) + util.MONITOR.waitForAbort(0.1) + self.setFocusId(self.BUTTON_IDS[0]) + + def onClick(self, controlID): + if controlID in self.BUTTON_IDS: + self.buttonChoice = self.BUTTON_IDS.index(controlID) + self.doClose() + + +def show(header, info, button0=None, button1=None, button2=None): + w = OptionsDialog.open(header=header, info=info, button0=button0, button1=button1, button2=button2) + choice = w.buttonChoice + del w + util.garbageCollect() + return choice diff --git a/script.plexmod/lib/windows/pagination.py b/script.plexmod/lib/windows/pagination.py new file mode 100644 index 000000000..474be481c --- /dev/null +++ b/script.plexmod/lib/windows/pagination.py @@ -0,0 +1,274 @@ +from __future__ import absolute_import +from . import kodigui +from kodi_six import xbmcgui +from lib import util + + +class MCLPaginator(object): + """ + A paginator for ManagedControlList instances + """ + control = None + pageSize = 8 + initialPageSize = 18 # + orphans = 26 + + # the amount of overhang allowed for both sides; don't show pagination when the overhang fits the current item count + orphans = pageSize + offset = 0 + leafCount = None + parentWindow = None + thumbFallback = None + + _direction = None + _currentAmount = None + _lastAmount = None + _boundaryHit = False + + def __init__(self, control, parent_window, page_size=None, orphans=None, leaf_count=None): + self.control = control + self.pageSize = page_size if page_size is not None else self.pageSize + self.orphans = orphans if orphans is not None else self.orphans + self.leafCount = leaf_count + self.parentWindow = parent_window + + self.reset() + + def reset(self): + self.offset = 0 + self._currentAmount = 0 + self._lastAmount = None + self._boundaryHit = False + self._direction = None + + def getData(self, offset, amount): + raise NotImplementedError + + def createListItem(self, data): + return self.parentWindow.createListItem(data) + + def prepareListItem(self, data, mli): + pass + + def readyForPaging(self): + return self.parentWindow.initialized + + @property + def _readyForPaging(self): + return self.readyForPaging() + + @property + def boundaryHit(self): + self._boundaryHit = False + + if not self._readyForPaging: + return + + mli = self.control.getSelectedItem() + if mli and mli.getProperty("is.boundary") and not mli.getProperty("is.updating"): + direction = "left" if mli.getProperty("left.boundary") else "right" + mli.setBoolProperty("is.updating", True) + self.offset = int(mli.getProperty("orig.index")) + self._direction = direction + self._boundaryHit = True + + return self._boundaryHit + + @property + def nextPage(self): + leafCount = self.leafCount + offset = self.offset + amount = self.pageSize + + if self._direction == "left": + if offset <= self.initialPageSize: + # return to initial page + offset = 0 + amount = self.initialPageSize + else: + # move the slice to the left by :amount: based on :offset: + amount = min(offset, self.pageSize) + offset -= amount + + # avoid short pages on the left end + if 0 < offset < self.orphans: + amount += offset + offset = 0 + + else: + # move the slice to the right + itemsLeft = leafCount - offset + # avoid short pages on the right end + if itemsLeft <= self.pageSize + self.orphans: + amount = self.pageSize + self.orphans + + self.offset = offset + data = self.getData(offset, amount) + self._lastAmount = self._currentAmount + self._currentAmount = len(data) + return data + + @property + def initialPage(self): + amount = self.initialPageSize + if self.initialPageSize + self.orphans >= self.leafCount: + amount = self.initialPageSize + self.orphans + + data = self.getData(self.offset, amount) + if data: + self._lastAmount = self._currentAmount + self._currentAmount = len(data) + return data + + def populate(self, items): + """ + Populates the current page to the bound Control List. Adds prev/next MLIs and selects the correct control + after doing so. + :param items: + :return: + """ + idx = 0 + moreLeft = self.offset > 0 + moreRight = self.offset + self._currentAmount < self.leafCount + + finalItems = [] + thumbFallback = self.thumbFallback + + if items: + for item in items: + mli = self.createListItem(item) + + if mli: + mli.setProperty('index', str(idx)) + self.prepareListItem(item, mli) + if thumbFallback: + if callable(thumbFallback): + mli.setProperty('thumb.fallback', thumbFallback(item)) + else: + mli.setProperty('thumb.fallback', thumbFallback) + + finalItems.append(mli) + idx += 1 + + if moreRight: + end = kodigui.ManagedListItem('') + end.setBoolProperty('is.boundary', True) + end.setBoolProperty('right.boundary', True) + end.setProperty("orig.index", str(int(self.offset + self._currentAmount))) + finalItems.append(end) + else: + # no boundary, rightmost item + finalItems[-1].setBoolProperty('last.item', True) + + if moreLeft: + start = kodigui.ManagedListItem('') + start.setBoolProperty('is.boundary', True) + start.setBoolProperty('left.boundary', True) + start.setProperty("orig.index", str(int(self.offset))) + finalItems.insert(0, start) + + self.control.replaceItems(finalItems) + self.selectItem(self._currentAmount, more_left=moreLeft, more_right=moreRight, items=items) + + return finalItems + + def selectItem(self, amount, more_left=False, more_right=False, items=None): + if self._direction: + if self._direction == "left": + self.control.selectItem(amount - (1 if not more_left else 0)) + return True + + elif self._direction == "right": + self.control.selectItem(1) + return True + + def paginate(self, force_page=False): + """ + Triggers the pagination for the currently selected view. In case of a hit boundary, show the next or previous + page, otherwise show the initial page. + :return: + """ + if self._boundaryHit or force_page: + items = self.nextPage + + else: + items = self.initialPage + + return self.populate(items) + + @property + def canSimpleWrap(self): + return self.initialPageSize + self.orphans >= self.leafCount + + def wrap(self, mli, last_mli, action): + """ + Wraps around the list if the first or last item is currently selected and the user requests to round robin. + :param mli: current item + :param last_mli: previous item + :param action: xbmcgui action + :return: + """ + + index = int(mli.getProperty("index")) + last_mli_index = int(last_mli.getProperty("index")) + + # special case for our absolute last item + is_rightmost = action == xbmcgui.ACTION_MOVE_RIGHT \ + and mli.getProperty('last.item') \ + and last_mli.getProperty('last.item') + + # _lastAmount is used to immediately wrap again after a wrap has happened; potentially an issue + if not is_rightmost and last_mli_index not in \ + (0, self._currentAmount - 1, (self._lastAmount - 1) if self._lastAmount else None) \ + or self._currentAmount < 2: + return + + onlyTwo = self._currentAmount == 2 + + items = None + if action == xbmcgui.ACTION_MOVE_LEFT and index == 0: + if onlyTwo and last_mli_index == self._currentAmount - 1: + return + + if not self.canSimpleWrap: + self._direction = "right" + self.offset = self.leafCount - self.orphans - self.pageSize + items = self.paginate(force_page=True) + self.control.selectItem(self._currentAmount) + else: + self.control.selectItem(self.leafCount - 1) + elif action == xbmcgui.ACTION_MOVE_RIGHT and (index == self._currentAmount - 1 or is_rightmost): + if onlyTwo and last_mli_index == 0: + return + + if not self.canSimpleWrap: + self._direction = "left" + self.offset = 0 + items = self.paginate() + self.control.selectItem(0) + + if items: + return items + + +class BaseRelatedPaginator(MCLPaginator): + initialPageSize = 8 + pageSize = initialPageSize + orphans = initialPageSize // 2 + + thumbFallback = lambda self, rel: 'script.plex/thumb_fallbacks/{0}.png'.format( + rel.type in ('show', 'season', 'episode') and 'show' or 'movie') + + def createListItem(self, rel): + return kodigui.ManagedListItem( + rel.title or '', + thumbnailImage=rel.defaultThumb.asTranscodedImageURL(*self.parentWindow.RELATED_DIM), + data_source=rel + ) + + def prepareListItem(self, data, mli): + if data.type in ('season', 'show'): + if not mli.dataSource.isWatched: + mli.setProperty('unwatched.count', str(mli.dataSource.unViewedLeafCount)) + else: + mli.setProperty('unwatched', not mli.dataSource.isWatched and '1' or '') + mli.setProperty('progress', util.getProgressImage(mli.dataSource)) diff --git a/script.plexmod/lib/windows/photos.py b/script.plexmod/lib/windows/photos.py new file mode 100644 index 000000000..df43a7e93 --- /dev/null +++ b/script.plexmod/lib/windows/photos.py @@ -0,0 +1,530 @@ +from __future__ import absolute_import +import threading +import time +import os +import tempfile +import shutil +import hashlib +import requests + +from kodi_six import xbmc, xbmcvfs +from kodi_six import xbmcgui + +from . import kodigui +from . import busy + +from lib import util, colors +from plexnet import plexapp, plexplayer, playqueue + + +class PhotoWindow(kodigui.BaseWindow): + xmlFile = 'script-plex-photo.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + OVERLAY_BUTTON_ID = 250 + OSD_GROUP_ID = 200 + + OSD_BUTTONS_GROUP_ID = 400 + REPEAT_BUTTON_ID = 401 + SHUFFLE_BUTTON_ID = 402 + ROTATE_BUTTON_ID = 403 + PREV_BUTTON_ID = 404 + PLAY_PAUSE_BUTTON_ID = 406 + STOP_BUTTON_ID = 407 + NEXT_BUTTON_ID = 409 + PQUEUE_BUTTON_ID = 412 + + PQUEUE_LIST_ID = 500 + PQUEUE_LIST_OVERLAY_BUTTON_ID = 501 + + SLIDESHOW_INTERVAL = util.slideshowInterval + + PHOTO_STACK_SIZE = 10 + tempSubFolder = ("p4k", "photos") + + def __init__(self, *args, **kwargs): + kodigui.BaseWindow.__init__(self, *args, **kwargs) + self.photo = kwargs.get('photo') + self.playQueue = kwargs.get('play_queue') + self.playerObject = None + self.timelineType = 'photo' + self.lastTimelineState = None + self.ignoreTimelines = False + self.trueTime = 0 + self.slideshowThread = None + self.slideshowRunning = False + self.slideshowNext = 0 + self.osdTimer = None + self.lastItem = None + self.showPhotoThread = None + self.showPhotoTimeout = 0 + self.rotate = 0 + self.tempFolder = None + self.photoStack = [] + self.initialLoad = True + + def onFirstInit(self): + self.tempFolder = os.path.join(util.translatePath("special://temp/"), *self.tempSubFolder) + + if not os.path.exists(self.tempFolder): + try: + os.makedirs(self.tempFolder) + except OSError: + if not os.path.isdir(self.tempFolder): + util.ERROR() + + self.pqueueList = kodigui.ManagedControlList(self, self.PQUEUE_LIST_ID, 14) + #self.setProperty('photo', 'script.plex/indicators/busy-photo.gif') + try: + self.getPlayQueue() + except AttributeError: + raise util.NoDataException + self.start() + self.osdTimer = kodigui.PropertyTimer(self._winID, 4, 'OSD', '', init_value=False, callback=self.osdTimerCallback) + self.imageControl = self.getControl(600) + + def osdTimerCallback(self): + self.setFocusId(self.OVERLAY_BUTTON_ID) + + def onAction(self, action): + try: + # controlID = self.getFocusId() + if action == xbmcgui.ACTION_MOVE_LEFT: + if not self.osdVisible() or self.getFocusId() == self.PQUEUE_LIST_OVERLAY_BUTTON_ID: + self.prev() + elif action == xbmcgui.ACTION_MOVE_RIGHT: + if not self.osdVisible() or self.getFocusId() == self.PQUEUE_LIST_OVERLAY_BUTTON_ID: + self.next() + elif action == xbmcgui.ACTION_MOVE_UP: + if self.osdVisible(): + if self.getFocusId() == self.OVERLAY_BUTTON_ID: + self.hideOSD() + else: + self.showOSD() + elif action == xbmcgui.ACTION_MOVE_DOWN: + if self.osdVisible(): + if self.getFocusId() == self.OVERLAY_BUTTON_ID: + self.hideOSD() + else: + self.showOSD() + elif action == xbmcgui.ACTION_STOP: + self.stop() + elif action in (xbmcgui.ACTION_PLAYER_PLAY, xbmcgui.ACTION_PAUSE): + if self.isPlaying(): + self.pause() + else: + self.play() + elif action == xbmcgui.ACTION_PREV_ITEM: + self.prev() + elif action == xbmcgui.ACTION_NEXT_ITEM: + self.next() + elif action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): + if self.osdVisible(): + self.hideOSD() + return + self.doClose() + return + + self.osdTimer.reset(init=False) + except: + util.ERROR() + + kodigui.BaseWindow.onAction(self, action) + + def checkPqueueListChanged(self): + item = self.pqueueList.getSelectedItem() + if item == self.lastItem: + return + + self.lastItem = item + self.onPqueueListChanged() + + def onClick(self, controlID): + if controlID == self.PREV_BUTTON_ID: + self.prev() + elif controlID == self.NEXT_BUTTON_ID: + next(self) + elif controlID == self.PLAY_PAUSE_BUTTON_ID: + if self.isPlaying(): + self.pause() + else: + self.play() + elif controlID == self.STOP_BUTTON_ID: + self.stop() + elif controlID == self.OVERLAY_BUTTON_ID: + self.showOSD() + elif controlID == self.SHUFFLE_BUTTON_ID: + self.shuffleButtonClicked() + elif controlID == self.REPEAT_BUTTON_ID: + self.repeatButtonClicked() + elif controlID == self.ROTATE_BUTTON_ID: + self.setRotation() + + def shuffleButtonClicked(self): + self.playQueue.setShuffle() + + def repeatButtonClicked(self): + if self.playQueue.isRepeat: + self.playQueue.setRepeat(False) + self.playQueue.refresh(force=True) + else: + self.playQueue.setRepeat(True) + self.playQueue.refresh(force=True) + + def setRotation(self, angle=None): + if angle is None: + self.resetSlideshowTimeout() + self.rotate += 90 + if self.rotate > 270: + self.rotate = 0 + else: + self.rotate = angle + + if self.rotate == 90: + self.imageControl.setPosition(420, -420) + self.imageControl.setWidth(1080) + self.imageControl.setHeight(1920) + elif self.rotate == 180: + self.imageControl.setPosition(0, 0) + self.imageControl.setWidth(1920) + self.imageControl.setHeight(1080) + elif self.rotate == 270: + self.imageControl.setPosition(420, -420) + self.imageControl.setWidth(1080) + self.imageControl.setHeight(1920) + else: + self.imageControl.setPosition(0, 0) + self.imageControl.setWidth(1920) + self.imageControl.setHeight(1080) + + self.setProperty('rotate', str(self.rotate)) + + def isPlaying(self): + return bool(self.getProperty('playing')) + + def getPlayQueue(self, shuffle=False): + if self.playQueue: + self.playQueue.on('items.changed', self.fillPqueueList) + self.playQueue.on('change', self.updateProperties) + self.updateProperties() + self.fillPqueueList() + else: + self.playQueue = playqueue.createPlayQueueForItem(self.photo, options={'shuffle': shuffle}) + self.playQueue.on('items.changed', self.fillPqueueList) + self.playQueue.on('change', self.updateProperties) + + util.DEBUG_LOG('waiting for playQueue to initialize') + if busy.widthDialog(self.playQueue.waitForInitialization, None): + util.DEBUG_LOG('playQueue initialized: {0}'.format(self.playQueue)) + else: + util.DEBUG_LOG('playQueue timed out wating for initialization') + + self.showPhoto() + + def fillPqueueList(self, **kwargs): + items = [] + for qi in self.playQueue.items(): + mli = kodigui.ManagedListItem(thumbnailImage=qi.thumb.asTranscodedImageURL(123, 123), data_source=qi) + items.append(mli) + + self.pqueueList.replaceItems(items) + self.updatePqueueListSelection() + + def updatePqueueListSelection(self, current=None): + selected = self.pqueueList.getListItemByDataSource(current or self.playQueue.current()) + if not selected or not selected.pos(): + return + + self.pqueueList.selectItem(selected.pos()) + + def showPhoto(self, trigger=None, **kwargs): + self.slideshowNext = 0 + + if not self.showPhotoThread or not self.showPhotoThread.is_alive(): + # if trigger is given, trigger it. trigger loads the next or prev item, depending on what was requested + # doing this here, this late prevents erratic behaviour when multiple next/prev calls were made but we were + # still loading images + if trigger: + trigger() + self.updateProperties() + + photo = self.playQueue.current() + + # fixme: video should work. + # bad temporary fix for videos in photo playqueues + if photo.type != "photo": + self.next() + util.DEBUG_LOG("SKIPPING PHOTO: %s" % photo) + return + + self.updatePqueueListSelection(photo) + + self.showPhotoTimeout = time.time() + 0.2 + if not self.showPhotoThread or not self.showPhotoThread.is_alive(): + self.showPhotoThread = threading.Thread(target=self._showPhoto, name="showphoto") + self.showPhotoThread.start() + + # wait for the current thread to end, which might still be loading the surrounding images, for 10 seconds + elif self.showPhotoThread.is_alive(): + waitedFor = 0 + self.setBoolProperty('is.updating', True) + while waitedFor < 100: + if not self.showPhotoThread.is_alive() and not util.MONITOR.abortRequested(): + return self.showPhoto(trigger=trigger, **kwargs) + elif util.MONITOR.abortRequested(): + self.setBoolProperty('is.updating', False) + return + + util.MONITOR.waitForAbort(0.1) + waitedFor += 0.1 + + # fixme raise error here + + def _showPhoto(self): + """ + load the current photo, preload the previous and the next one + :return: + """ + photo = self.playQueue.current() + next = self.playQueue.getNext() + loadItems = (photo, next, self.playQueue.getPrev()) + for item in loadItems: + if item is not None: + item.softReload() + + self.playerObject = plexplayer.PlexPhotoPlayer(photo) + + addToStack = [] + currentFailed = False + try: + for item in loadItems: + if not item: + continue + + if item.type != "photo": + continue + + meta = self.playerObject.build(item=item) + url = photo.server.getImageTranscodeURL(meta.get('url', ''), self.width, self.height) + bgURL = item.thumb.asTranscodedImageURL(self.width, self.height, blur=128, opacity=60, + background=colors.noAlpha.Background) + + isCurrent = currentFailed or item == photo + if isCurrent and not self.initialLoad: + self.setBoolProperty('is.updating', True) + + path, background = self.getCachedPhotoData(url, bgURL) + if not (path and background): + currentFailed = True + continue + + if (path, background) not in self.photoStack: + if item == next: + # move the next image to the top of the stack + addToStack.insert(0, (path, background)) + else: + addToStack.append((path, background)) + + if isCurrent: + self._reallyShowPhoto(item, path, background) + self.setBoolProperty('is.updating', False) + self.initialLoad = False + + # maintain cache folder + self.photoStack = addToStack + self.photoStack + if len(self.photoStack) > self.PHOTO_STACK_SIZE: + clean = self.photoStack[self.PHOTO_STACK_SIZE:] + self.photoStack = self.photoStack[:self.PHOTO_STACK_SIZE] + for remList in clean: + for rem in remList: + try: + os.remove(rem) + except: + pass + finally: + self.setBoolProperty('is.updating', False) + + def getCachedPhotoData(self, url, bgURL): + if not url: + return + + basename = hashlib.sha1(url.encode('utf-8')).hexdigest() + tmpPath = os.path.join(self.tempFolder, basename) + tmpBgPath = os.path.join(self.tempFolder, "%s_bg" % basename) + + for p, url in ((tmpPath, url), (tmpBgPath, bgURL)): + if not os.path.exists(p):# and not xbmc.getCacheThumbName(tmpFn): + try: + r = requests.get(url, allow_redirects=True, timeout=10.0) + r.raise_for_status() + except Exception as e: + util.ERROR("Couldn't load image: %s" % e, notify=True) + return None, None + else: + try: + with open(p, 'wb') as f: + f.write(r.content) + except: + return None, None + + return tmpPath, tmpBgPath + + def _reallyShowPhoto(self, photo, path, background): + self.setRotation(0) + self.setProperty('photo', path) + self.setProperty('background', background) + + self.setProperty('photo.title', photo.title) + self.setProperty('photo.date', util.cleanLeadingZeros(photo.originallyAvailableAt.asDatetime('%d %B %Y'))) + self.setProperty('camera.model', photo.media[0].model) + self.setProperty('camera.lens', photo.media[0].lens) + + if photo.media[0].height: + dims = u'{0} x {1}{2}'.format( + photo.media[0].width, + photo.media[0].height, + photo.media[0].parts[0].orientation and u' \u2022 {0} Mo'.format(photo.media[0].parts[0].orientation) or '' + ) + self.setProperty('photo.dims', dims) + settings = [] + if photo.media[0].iso: + settings.append('ISO {0}'.format(photo.media[0].iso)) + if photo.media[0].aperture: + settings.append('{0}'.format(photo.media[0].aperture)) + if photo.media[0].exposure: + settings.append('{0}'.format(photo.media[0].exposure)) + self.setProperty('camera.settings', u' \u2022 '.join(settings)) + self.setProperty('photo.summary', photo.summary) + container = photo.media[0].container_ or os.path.splitext(photo.media[0].parts[0].file)[-1][1:].lower() + if container == 'jpg': + container = 'jpeg' + self.setProperty('photo.container', container) + self.updateNowPlaying(force=True, refreshQueue=True) + self.resetSlideshowTimeout() + + def updateProperties(self, **kwargs): + self.setProperty('pq.shuffled', self.playQueue.isShuffled and '1' or '') + self.setProperty('pq.repeat', self.playQueue.isRepeat and '1' or '') + if not self.getProperty('hide.prev') and not self.playQueue.hasPrev(): + if self.playQueue.hasNext(): + self.setFocusId(self.NEXT_BUTTON_ID) + else: + self.setFocusId(self.PLAY_PAUSE_BUTTON_ID) + self.setProperty('hide.prev', not self.playQueue.hasPrev() and '1' or '') + if not self.getProperty('hide.next') and not self.playQueue.hasNext(): + if self.playQueue.hasPrev(): + self.setFocusId(self.PREV_BUTTON_ID) + else: + self.setFocusId(self.PLAY_PAUSE_BUTTON_ID) + self.setProperty('hide.next', not self.playQueue.hasNext() and '1' or '') + + def slideshow(self): + util.DEBUG_LOG('Slideshow: STARTED') + self.slideshowRunning = True + + # inhibit screensaver in matrix and above + if util.KODI_VERSION_MAJOR > 18: + xbmc.executebuiltin('InhibitScreensaver(true)') + + self.resetSlideshowTimeout() + while not util.MONITOR.waitForAbort(0.1) and self.slideshowRunning: + if not self.slideshowNext or time.time() < self.slideshowNext: + continue + self.next() + + if util.KODI_VERSION_MAJOR > 18: + xbmc.executebuiltin('InhibitScreensaver(false)') + + util.DEBUG_LOG('Slideshow: STOPPED') + + def resetSlideshowTimeout(self): + self.slideshowNext = time.time() + self.SLIDESHOW_INTERVAL + + def osdVisible(self): + return self.getProperty('OSD') + + def pqueueVisible(self): + return self.getProperty('show.pqueue') + + def start(self): + self.setFocusId(self.OVERLAY_BUTTON_ID) + + def prev(self): + if not self.playQueue.getPrev(): + return + self.showPhoto(trigger=lambda: self.playQueue.prev()) + + def next(self): + if not self.playQueue.getNext(): + return + self.showPhoto(trigger=lambda: self.playQueue.next()) + + __next__ = next + + def play(self): + self.setProperty('playing', '1') + if self.slideshowThread and self.slideshowThread.is_alive(): + return + + self.slideshowThread = threading.Thread(target=self.slideshow, name='slideshow') + self.slideshowThread.start() + + def pause(self): + self.setProperty('playing', '') + self.slideshowRunning = False + + def stop(self): + self.doClose() + + def doClose(self): + self.pause() + shutil.rmtree(self.tempFolder, ignore_errors=True) + + kodigui.BaseWindow.doClose(self) + + def getCurrentItem(self): + if self.playerObject: + return self.playerObject.item + return None + + def shouldSendTimeline(self, item): + return item.ratingKey and item.getServer() + + def updateNowPlaying(self, force=False, refreshQueue=False, state=None): + if self.ignoreTimelines: + return + + item = self.getCurrentItem() + + if not item: + return + + if not self.shouldSendTimeline(item): + return + + state = state or 'paused' + # Avoid duplicates + if state == self.lastTimelineState and not force: + return + + self.lastTimelineState = state + # self.timelineTimer.reset() + + time = int(self.trueTime * 1000) + + # self.trigger("progress", [m, item, time]) + + if refreshQueue and self.playQueue: + self.playQueue.refreshOnTimeline = True + + plexapp.util.APP.nowplayingmanager.updatePlaybackState(self.timelineType, self.playerObject, state, time, self.playQueue) + + def showOSD(self): + self.osdTimer.reset(init=False) + + def hideOSD(self, closing=False): + self.osdTimer.stop(trigger=True) diff --git a/script.plexmod/lib/windows/playbacksettings.py b/script.plexmod/lib/windows/playbacksettings.py new file mode 100644 index 000000000..5e614e1ae --- /dev/null +++ b/script.plexmod/lib/windows/playbacksettings.py @@ -0,0 +1,64 @@ +# coding=utf-8 +from lib import util +from lib.util import T + +from plexnet.util import INTERFACE + +from . import dropdown + + +class PlaybackSettingsMixin(object): + def playbackSettings(self, show, pos, bottom): + pbs = INTERFACE.playbackManager(show) + pbOpts = [] + transEnabled = T(33507) + transDisabled = T(33508) + + currentSettings = {} + for key, transID in INTERFACE.playbackManager.transMap.items(): + state = getattr(pbs, key) + stateTrans = state and transEnabled or transDisabled + pbOpts.append({'key': key, 'display': "{}: {}".format(stateTrans, T(transID))}) + currentSettings[key] = state + + # create a new dict which we can freely change while the dropdown is open + newSettings = currentSettings.copy() + + # fixme: not sure if garbage collection is necessary here + + def callback(opts, mli, force_off=False): + choice = mli.dataSource + oc = newSettings[choice["key"]] + ic = (not oc) if not force_off else False + + # invalidate any other setting if bingemode enabled + if choice["key"] == "binge_mode" and ic: + for m in opts.items: + if m.dataSource["key"] != "binge_mode": + callback(opts, m, force_off=True) + del m + + # disable bingeMode if any other setting is enabled + elif choice["key"] != "binge_mode" and newSettings["binge_mode"]: + m = opts.getListItem(0) + callback(opts, m, force_off=True) + del m + + newSettings[choice["key"]] = ic + label = choice["display"].replace("{}: ".format(oc and transEnabled or transDisabled), + "{}: ".format(ic and transEnabled or transDisabled)) + mli.setLabel(label) + mli.dataSource["display"] = label + del mli + + # fixme: not sure if garbage collection is necessary here + util.garbageCollect() + pbChoice = dropdown.showDropdown(pbOpts, pos, pos_is_bottom=bottom, close_direction='left', + set_dropdown_prop=True, + header="{}: {}".format(T(32925, 'Playback Settings'), show.defaultTitle), + close_only_with_back=True, align_items='left', + options_callback=callback) + + # write new settings for item if necessary + if newSettings != currentSettings: + INTERFACE.playbackManager(show, kv_dict=newSettings) diff --git a/script.plexmod/lib/windows/playerbackground.py b/script.plexmod/lib/windows/playerbackground.py new file mode 100644 index 000000000..e34073b15 --- /dev/null +++ b/script.plexmod/lib/windows/playerbackground.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import +import contextlib +from . import kodigui +from lib import util + + +class PlayerBackground(kodigui.BaseWindow): + xmlFile = 'script-plex-player_background.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + def __init__(self, *args, **kwargs): + kodigui.BaseWindow.__init__(self, *args, **kwargs) + self.background = kwargs.get('background', '') + self._closedSet = False + + def onFirstInit(self): + self.setProperty('background', self.background) + + def onReInit(self): + if self._closedSet: + self._closedSet = False + self.doClose() + + @contextlib.contextmanager + def asContext(self): + self.show() + yield + self.doClose() + + def setClosed(self): + self._closedSet = True + self.doClose() + + +class PlayerBackgroundContext(object): + def __init__(self, **kwargs): + self.kwargs = kwargs + self.window = None + + def __enter__(self): + self.window = PlayerBackground.create(**self.kwargs) + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + if self.window: + self.window.doClose() + del self.window + self.window = None diff --git a/script.plexmod/lib/windows/playersettings.py b/script.plexmod/lib/windows/playersettings.py new file mode 100644 index 000000000..0ad1c58ed --- /dev/null +++ b/script.plexmod/lib/windows/playersettings.py @@ -0,0 +1,340 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from lib import util +from lib import metadata +from lib.util import T + +import plexnet + + +class VideoSettingsDialog(kodigui.BaseDialog, util.CronReceiver): + xmlFile = 'script-plex-video_settings_dialog.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + SETTINGS_LIST_ID = 100 + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + self.video = kwargs.get('video') + self.viaOSD = kwargs.get('via_osd') + self.nonPlayback = kwargs.get('non_playback') + self.parent = kwargs.get('parent') + self.roundRobin = kwargs.get('round_robin', True) + self.lastSelectedItem = 0 + + if not self.video.mediaChoice: + playerObject = plexnet.plexplayer.PlexPlayer(self.video) + playerObject.build() + + def onFirstInit(self): + self.settingsList = kodigui.ManagedControlList(self, self.SETTINGS_LIST_ID, 6) + self.setProperty('heading', T(32343, 'Settings')) + if self.viaOSD: + self.setProperty('via.OSD', '1') + self.showSettings(True) + util.CRON.registerReceiver(self) + + def onAction(self, action): + try: + if not xbmc.getCondVisibility('Player.HasMedia') and not self.nonPlayback: + self.doClose() + return + except: + util.ERROR() + + if self.roundRobin and action in (xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_MOVE_DOWN) and \ + self.getFocusId() == self.SETTINGS_LIST_ID: + to_pos = None + last_index = self.settingsList.size() - 1 + if action == xbmcgui.ACTION_MOVE_UP and self.lastSelectedItem == 0 and self.settingsList.topHasFocus(): + to_pos = last_index + + elif action == xbmcgui.ACTION_MOVE_DOWN and self.lastSelectedItem == last_index \ + and self.settingsList.bottomHasFocus(): + to_pos = 0 + + if to_pos is not None: + self.settingsList.setSelectedItemByPos(to_pos) + self.lastSelectedItem = to_pos + return + + self.lastSelectedItem = self.settingsList.control.getSelectedPosition() + + kodigui.BaseDialog.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.SETTINGS_LIST_ID: + self.editSetting() + + def onClosed(self): + util.CRON.cancelReceiver(self) + + def tick(self): + if self.nonPlayback: + return + + if not xbmc.getCondVisibility('Player.HasMedia'): + self.doClose() + return + + def showSettings(self, init=False): + video = self.video + override = video.settings.getPrefOverride('local_quality') + if override is not None and override < 13: + current = T((32001, 32002, 32003, 32004, 32005, 32006, 32007, 32008, 32009, 32010, 32011, 32012, 32013, 32014)[13 - override]) + else: + current = u'{0} {1} ({2})'.format( + plexnet.util.bitrateToString(video.mediaChoice.media.bitrate.asInt() * 1000), + video.mediaChoice.media.getVideoResolutionString(), + video.mediaChoice.media.title or 'Original' + ) + + audio, subtitle = self.getAudioAndSubtitleInfo() + + options = [ + ('audio', T(32395, 'Audio'), audio), + ('subs', T(32396, 'Subtitles'), subtitle), + ('quality', T(32397, 'Quality'), u'{0}'.format(current)) + ] + + if not self.nonPlayback: + options += [ + ('kodi_video', T(32398, 'Kodi Video Settings'), ''), + ('kodi_audio', T(32399, 'Kodi Audio Settings'), ''), + ] + if util.KODI_VERSION_MAJOR >= 18: + options.append(('kodi_subtitle', T(32492, 'Kodi Subtitle Settings'), '')) + + if self.viaOSD: + if self.parent.getProperty("show.PPI"): + options += [ + ('stream_info', T(32483, 'Hide Stream Info'), ''), + ] + else: + options += [ + ('stream_info', T(32484, 'Show Stream Info'), ''), + ] + + items = [] + for o in options: + item = kodigui.ManagedListItem(o[1], o[2], data_source=o[0]) + items.append(item) + if init: + self.settingsList.reset() + self.settingsList.addItems(items) + else: + self.settingsList.replaceItems(items) + + self.setFocusId(self.SETTINGS_LIST_ID) + + def getAudioAndSubtitleInfo(self): + sas = self.video.selectedAudioStream() + audio = sas and sas.getTitle(metadata.apiTranslate) or T(32309, 'None') + + sss = self.video.selectedSubtitleStream( + forced_subtitles_override=util.getSetting("forced_subtitles_override", False)) + if sss: + if len(self.video.subtitleStreams) > 1: + subtitle = u'{0} \u2022 {1} {2}'.format(sss.getTitle(metadata.apiTranslate), len(self.video.subtitleStreams) - 1, T(32307, 'More')) + else: + subtitle = sss.getTitle(metadata.apiTranslate) + else: + if self.video.subtitleStreams: + subtitle = u'{0} \u2022 {1} {2}'.format(T(32309, 'None'), len(self.video.subtitleStreams), T(32308, 'Available')) + else: + subtitle = T(32309, 'None') + + return audio, subtitle + + def editSetting(self): + mli = self.settingsList.getSelectedItem() + if not mli: + return + + result = mli.dataSource + + if result == 'audio': + showAudioDialog(self.video, non_playback=self.nonPlayback) + elif result == 'subs': + showSubtitlesDialog(self.video, non_playback=self.nonPlayback) + elif result == 'quality': + idx = None + override = self.video.settings.getPrefOverride('local_quality') + if override is not None and override < 13: + idx = 13 - override + showQualityDialog(self.video, non_playback=self.nonPlayback, selected_idx=idx) + elif result == 'kodi_video': + xbmc.executebuiltin('ActivateWindow(OSDVideoSettings)') + elif result == 'kodi_audio': + xbmc.executebuiltin('ActivateWindow(OSDAudioSettings)') + elif result == 'kodi_subtitle': + xbmc.executebuiltin('ActivateWindow(OSDSubtitleSettings)') + elif result == "stream_info": + if self.parent: + if self.parent.getProperty("show.PPI"): + self.parent.hidePPIDialog() + else: + #xbmc.executebuiltin('Action(PlayerProcessInfo)') + self.parent.showPPIDialog() + self.doClose() + return + + self.showSettings() + + +class SelectDialog(kodigui.BaseDialog, util.CronReceiver): + xmlFile = 'script-plex-settings_select_dialog.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + OPTIONS_LIST_ID = 100 + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + self.heading = kwargs.get('heading') + self.options = kwargs.get('options') + self.selectedIdx = kwargs.get('selected_idx') + self.choice = None + self.nonPlayback = kwargs.get('non_playback') + self.lastSelectedItem = self.selectedIdx if self.selectedIdx is not None else 0 + self.roundRobin = kwargs.get('round_robin', True) + + def onFirstInit(self): + self.optionsList = kodigui.ManagedControlList(self, self.OPTIONS_LIST_ID, 8) + self.setProperty('heading', self.heading) + self.showOptions() + util.CRON.registerReceiver(self) + + def onAction(self, action): + try: + if not xbmc.getCondVisibility('Player.HasMedia') and not self.nonPlayback: + self.doClose() + return + except: + util.ERROR() + + if self.roundRobin and action in (xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_MOVE_DOWN) and \ + self.getFocusId() == self.OPTIONS_LIST_ID: + to_pos = None + last_index = self.optionsList.size() - 1 + + if last_index > 0: + if action == xbmcgui.ACTION_MOVE_UP and self.lastSelectedItem == 0 and self.optionsList.topHasFocus(): + to_pos = last_index + + elif action == xbmcgui.ACTION_MOVE_DOWN and self.lastSelectedItem == last_index \ + and self.optionsList.bottomHasFocus(): + to_pos = 0 + + if to_pos is not None: + self.optionsList.setSelectedItemByPos(to_pos) + self.lastSelectedItem = to_pos + return + + self.lastSelectedItem = self.optionsList.control.getSelectedPosition() + + kodigui.BaseDialog.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.OPTIONS_LIST_ID: + self.setChoice() + + def onClosed(self): + util.CRON.cancelReceiver(self) + + def tick(self): + if self.nonPlayback: + return + + if not xbmc.getCondVisibility('Player.HasMedia'): + self.doClose() + return + + def setChoice(self): + mli = self.optionsList.getSelectedItem() + if not mli: + return + + self.choice = self.options[self.optionsList.getSelectedPosition()][0] + self.doClose() + + def showOptions(self): + items = [] + for o in self.options: + item = kodigui.ManagedListItem(o[1], data_source=o[0]) + items.append(item) + + self.optionsList.reset() + self.optionsList.addItems(items) + + if self.selectedIdx is not None: + self.optionsList.selectItem(self.selectedIdx) + + self.setFocusId(self.OPTIONS_LIST_ID) + + +def showOptionsDialog(heading, options, non_playback=False, selected_idx=None): + w = SelectDialog.open(heading=heading, options=options, non_playback=non_playback, selected_idx=selected_idx) + choice = w.choice + del w + util.garbageCollect() + return choice + + +def showAudioDialog(video, non_playback=False): + options = [] + idx = None + for i, s in enumerate(video.audioStreams): + if s.isSelected(): + idx = i + options.append((s, s.getTitle(metadata.apiTranslate))) + choice = showOptionsDialog(T(32395, 'Audio'), options, non_playback=non_playback, selected_idx=idx) + if choice is None: + return + + video.selectStream(choice) + + +def showSubtitlesDialog(video, non_playback=False): + options = [(plexnet.plexstream.NoneStream(), 'None')] + idx = None + for i, s in enumerate(video.subtitleStreams): + if s.isSelected(): + idx = i + 1 + options.append((s, s.getTitle(metadata.apiTranslate))) + + choice = showOptionsDialog(T(32396, 'Subtitle'), options, non_playback=non_playback, selected_idx=idx) + if choice is None: + return + + video.selectStream(choice) + video.manually_selected_sub_stream = choice.id + + +def showQualityDialog(video, non_playback=False, selected_idx=None): + options = [(13 - i, T(l)) for (i, l) in enumerate((32001, 32002, 32003, 32004, 32005, 32006, 32007, 32008, 32009, + 32010, 32011))] + + choice = showOptionsDialog('Quality', options, non_playback=non_playback, selected_idx=selected_idx) + if choice is None: + return + + video.settings.setPrefOverride('local_quality', choice) + video.settings.setPrefOverride('remote_quality', choice) + video.settings.setPrefOverride('online_quality', choice) + + +def showDialog(video, non_playback=False, via_osd=False, parent=None): + w = VideoSettingsDialog.open(video=video, non_playback=non_playback, via_osd=via_osd, parent=parent) + del w + util.garbageCollect() diff --git a/script.plexmod/lib/windows/playlist.py b/script.plexmod/lib/windows/playlist.py new file mode 100644 index 000000000..705fed372 --- /dev/null +++ b/script.plexmod/lib/windows/playlist.py @@ -0,0 +1,325 @@ +from __future__ import absolute_import +import threading + +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from . import busy +from . import videoplayer +from . import windowutils +from . import dropdown +from . import search +import plexnet +from . import opener + +from lib import colors +from lib import util +from lib import player +from lib import backgroundthread + +from lib.util import T +from six.moves import range + +PLAYLIST_PAGE_SIZE = 500 +PLAYLIST_INITIAL_SIZE = 100 + + +class ChunkRequestTask(backgroundthread.Task): + WINDOW = None + + @classmethod + def reset(cls): + del cls.WINDOW + cls.WINDOW = None + + def setup(self, start, size): + self.start = start + self.size = size + return self + + def contains(self, pos): + return self.start <= pos <= (self.start + self.size) + + def run(self): + if self.isCanceled(): + return + + try: + items = self.WINDOW.playlist.extend(self.start, self.size) + if self.isCanceled(): + return + + if not self.WINDOW: # Window is closed + return + + self.WINDOW.chunkCallback(items, self.start) + except AttributeError: + util.DEBUG_LOG('Playlist window closed, ignoring chunk at index {0}'.format(self.start)) + except plexnet.exceptions.BadRequest: + util.DEBUG_LOG('404 on playlist: {0}'.format(repr(self.WINDOW.playlist.title))) + + +class PlaylistWindow(kodigui.ControlledWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-playlist.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + OPTIONS_GROUP_ID = 200 + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + PLAYER_STATUS_BUTTON_ID = 204 + + PLAY_BUTTON_ID = 301 + SHUFFLE_BUTTON_ID = 302 + OPTIONS_BUTTON_ID = 303 + + LI_AR16X9_THUMB_DIM = util.scaleResolution(178, 100) + LI_SQUARE_THUMB_DIM = util.scaleResolution(100, 100) + + ALBUM_THUMB_DIM = util.scaleResolution(630, 630) + + PLAYLIST_LIST_ID = 101 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.playlist = kwargs.get('playlist') + self.exitCommand = None + self.tasks = backgroundthread.Tasks() + self.isPlaying = False + ChunkRequestTask.WINDOW = self + + def onFirstInit(self): + self.playlistListControl = kodigui.ManagedControlList(self, self.PLAYLIST_LIST_ID, 5) + self.setProperties() + + self.fillPlaylist() + self.setFocusId(self.PLAYLIST_LIST_ID) + + # def onAction(self, action): + # try: + # if action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + # if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + # self.setFocusId(self.OPTIONS_GROUP_ID) + # return + # except: + # util.ERROR() + + # kodigui.ControlledWindow.onAction(self, action) + + def onAction(self, action): + try: + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU): + self.doClose() + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.PLAYLIST_LIST_ID: + self.playlistListClicked() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.PLAY_BUTTON_ID: + self.playlistListClicked(no_item=True, shuffle=False) + elif controlID == self.SHUFFLE_BUTTON_ID: + self.playlistListClicked(no_item=True, shuffle=True) + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked() + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def doClose(self): + kodigui.ControlledWindow.doClose(self) + self.tasks.cancel() + ChunkRequestTask.reset() + + def searchButtonClicked(self): + self.processCommand(search.dialog(self)) + + def playlistListClicked(self, no_item=False, shuffle=False): + if no_item: + mli = None + else: + mli = self.playlistListControl.getSelectedItem() + if not mli or not mli.dataSource: + return + + try: + self.isPlaying = True + self.tasks.cancel() + player.PLAYER.stop() # Necessary because if audio is already playing, it will close the window when that is stopped + if self.playlist.playlistType == 'audio': + if self.playlist.leafCount.asInt() <= PLAYLIST_INITIAL_SIZE: + self.playlist.setShuffle(shuffle) + self.playlist.setCurrent(mli and mli.pos() or 0) + self.showAudioPlayer(track=mli and mli.dataSource or self.playlist.current(), playlist=self.playlist) + else: + args = {'sourceType': '8', 'shuffle': shuffle} + if mli: + args['key'] = mli.dataSource.key + pq = plexnet.playqueue.createPlayQueueForItem(self.playlist, options=args) + opener.open(pq) + elif self.playlist.playlistType == 'video': + if not util.advancedSettings.playlistVisitMedia: + if self.playlist.leafCount.asInt() <= PLAYLIST_INITIAL_SIZE: + self.playlist.setShuffle(shuffle) + self.playlist.setCurrent(mli and mli.pos() or 0) + videoplayer.play(play_queue=self.playlist) + else: + args = {'shuffle': shuffle} + if mli: + args['key'] = mli.dataSource.key + pq = plexnet.playqueue.createPlayQueueForItem(self.playlist, options=args) + opener.open(pq) + else: + if not mli: + firstItem = 0 + if shuffle: + import random + firstItem = random.randint(0, self.playlistListControl.size()-1) + mli = self.playlistListControl.getListItem(firstItem) + self.openItem(mli.dataSource) + + finally: + self.isPlaying = False + self.restartFill() + + def restartFill(self): + threading.Thread(target=self._restartFill).start() + + def _restartFill(self): + util.DEBUG_LOG('Checking if playlist list is full...') + for idx, mli in enumerate(self.playlistListControl): + if self.isPlaying or not self.isOpen or util.MONITOR.abortRequested(): + break + + if not mli.dataSource: + if self.playlist[idx]: + self.updateListItem(idx, self.playlist[idx]) + else: + break + # Update the progress for videos + elif mli.dataSource.type in ('episode', 'movie', 'clip'): + mli.dataSource.reload() + self.updateListItem(idx, mli.dataSource) + else: + util.DEBUG_LOG('Playlist list is full - nothing to do') + return + + util.DEBUG_LOG('Playlist list is not full - finishing') + total = self.playlist.leafCount.asInt() + for start in range(idx, total, PLAYLIST_PAGE_SIZE): + if util.MONITOR.abortRequested(): + break + self.tasks.add(ChunkRequestTask().setup(start, PLAYLIST_PAGE_SIZE)) + + backgroundthread.BGThreader.addTasksToFront(self.tasks) + + def optionsButtonClicked(self): + options = [] + if xbmc.getCondVisibility('Player.HasAudio + MusicPlayer.HasNext'): + options.append({'key': 'play_next', 'display': T(32325, 'Play Next')}) + + if not options: + return + + choice = dropdown.showDropdown(options, (440, 1020), close_direction='down', pos_is_bottom=True, close_on_playback_ended=True) + if not choice: + return + + if choice['key'] == 'play_next': + xbmc.executebuiltin('PlayerControl(Next)') + + def setProperties(self): + self.setProperty( + 'background', + util.backgroundFromArt(self.playlist.composite, width=self.width, height=self.height) + ) + self.setProperty('playlist.thumb', self.playlist.composite.asTranscodedImageURL(*self.ALBUM_THUMB_DIM)) + self.setProperty('playlist.title', self.playlist.title) + self.setProperty('playlist.duration', util.durationToText(self.playlist.duration.asInt())) + + def updateListItem(self, idx, pi, mli=None): + mli = mli or self.playlistListControl.getListItem(idx) + mli.setLabel(pi.title) + mli.setProperty('track.ID', pi.ratingKey) + mli.setProperty('track.number', str(idx + 1)) + mli.dataSource = pi + + if pi.type == 'track': + self.createTrackListItem(mli, pi) + elif pi.type == 'episode': + self.createEpisodeListItem(mli, pi) + elif pi.type in ('movie', 'clip'): + self.createMovieListItem(mli, pi) + + if pi.type in ('episode', 'movie', 'clip'): + mli.setProperty('progress', util.getProgressImage(mli.dataSource)) + + return mli + + def createTrackListItem(self, mli, track): + mli.setLabel2(u'{0} / {1}'.format(track.grandparentTitle, track.parentTitle)) + mli.setThumbnailImage(track.defaultThumb.asTranscodedImageURL(*self.LI_SQUARE_THUMB_DIM)) + mli.setProperty('track.duration', util.simplifiedTimeDisplay(track.duration.asInt())) + + def createEpisodeListItem(self, mli, episode): + label2 = u'{0} \u2022 {1}'.format( + episode.grandparentTitle, u'{0}{1} \u2022 {2}{3}'.format(T(32310, 'S'), episode.parentIndex, T(32311, 'E'), episode.index) + ) + mli.setLabel2(label2) + mli.setThumbnailImage(episode.thumb.asTranscodedImageURL(*self.LI_AR16X9_THUMB_DIM)) + mli.setProperty('track.duration', util.durationToShortText(episode.duration.asInt())) + mli.setProperty('video', '1') + mli.setProperty('watched', episode.isWatched and '1' or '') + + def createMovieListItem(self, mli, movie): + mli.setLabel(movie.defaultTitle) + mli.setLabel2(movie.year) + mli.setThumbnailImage(movie.art.asTranscodedImageURL(*self.LI_AR16X9_THUMB_DIM)) + mli.setProperty('track.duration', util.durationToShortText(movie.duration.asInt())) + mli.setProperty('video', '1') + mli.setProperty('watched', movie.isWatched and '1' or '') + + @busy.dialog() + def fillPlaylist(self): + total = self.playlist.leafCount.asInt() + + # leafCount is clamped to 6 when coming from Home/PlaylistsHub + actualPlaylistLength = len(self.playlist.items()) + + if total < len(self.playlist): + total = actualPlaylistLength + + endoffirst = min(PLAYLIST_INITIAL_SIZE, PLAYLIST_PAGE_SIZE, total) + items = [self.updateListItem(i, pi, kodigui.ManagedListItem()) for i, pi in enumerate(self.playlist.extend(0, endoffirst))] + + items += [kodigui.ManagedListItem() for i in range(total - endoffirst)] + + self.playlistListControl.reset() + self.playlistListControl.addItems(items) + + if total <= min(PLAYLIST_INITIAL_SIZE, PLAYLIST_PAGE_SIZE): + return + + for start in range(endoffirst, total, PLAYLIST_PAGE_SIZE): + if util.MONITOR.abortRequested(): + break + self.tasks.add(ChunkRequestTask().setup(start, PLAYLIST_PAGE_SIZE)) + + backgroundthread.BGThreader.addTasksToFront(self.tasks) + + def chunkCallback(self, items, start): + for i, pi in enumerate(items): + if self.isPlaying or not self.isOpen or util.MONITOR.abortRequested(): + break + + idx = start + i + self.updateListItem(idx, pi) diff --git a/script.plexmod/lib/windows/playlists.py b/script.plexmod/lib/windows/playlists.py new file mode 100644 index 000000000..12d22bb1f --- /dev/null +++ b/script.plexmod/lib/windows/playlists.py @@ -0,0 +1,138 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from . import busy +from . import playlist +from . import windowutils +from . import search + +from lib import util +from lib import colors + +from plexnet import plexapp + + +class PlaylistsWindow(kodigui.ControlledWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-playlists.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + THUMB_DIMS = { + 'audio': { + 'item.thumb': util.scaleResolution(270, 270) + }, + 'video': { + 'item.thumb': util.scaleResolution(610, 344) + } + } + + AUDIO_PL_LIST_ID = 101 + VIDEO_PL_LIST_ID = 301 + + OPTIONS_GROUP_ID = 200 + + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + PLAYER_STATUS_BUTTON_ID = 204 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.exitCommand = None + + def onFirstInit(self): + self.audioPLListControl = kodigui.ManagedControlList(self, self.AUDIO_PL_LIST_ID, 5) + self.videoPLListControl = kodigui.ManagedControlList(self, self.VIDEO_PL_LIST_ID, 5) + + self.fill() + if self.audioPLListControl.size(): + self.setFocusId(self.AUDIO_PL_LIST_ID) + else: + self.setFocusId(self.VIDEO_PL_LIST_ID) + + def onAction(self, action): + try: + if action == xbmcgui.ACTION_CONTEXT_MENU: + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + self.setFocusId(self.OPTIONS_GROUP_ID) + return + # elif action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + # if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + # self.setFocusId(self.OPTIONS_GROUP_ID) + # return + + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.AUDIO_PL_LIST_ID: + self.playlistListClicked(self.audioPLListControl) + elif controlID == self.VIDEO_PL_LIST_ID: + self.playlistListClicked(self.videoPLListControl) + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def searchButtonClicked(self): + self.processCommand(search.dialog(self)) + + def playlistListClicked(self, list_control): + mli = list_control.getSelectedItem() + if not mli: + return + + self.openWindow(playlist.PlaylistWindow, playlist=mli.dataSource) + + def createListItem(self, obj): + dimensions = self.THUMB_DIMS.get(obj.playlistType) + + if not dimensions: + return + + w, h = dimensions['item.thumb'] + + if obj.playlistType == 'audio': + thumb = obj.buildComposite(width=w, height=h, media='thumb') + else: + thumb = obj.buildComposite(width=w, height=h, media='art') + + mli = kodigui.ManagedListItem( + obj.title or '', + util.durationToText(obj.duration.asInt()), + # thumbnailImage=obj.composite.asTranscodedImageURL(*self.THUMB_DIMS[obj.playlistType]['item.thumb']), + thumbnailImage=thumb, + data_source=obj + ) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/{0}.png'.format(obj.playlistType == 'audio' and 'music' or 'movie')) + + return mli + + @busy.dialog() + def fill(self): + items = { + 'audio': [], + 'video': [] + } + playlists = plexapp.SERVERMANAGER.selectedServer.playlists() + + self.setProperty( + 'background', + util.backgroundFromArt(playlists[0].composite, width=self.width, height=self.height) + ) + + for pl in playlists: + mli = self.createListItem(pl) + if mli: + items[pl.playlistType].append(mli) + + self.audioPLListControl.addItems(items['audio']) + self.videoPLListControl.addItems(items['video']) diff --git a/script.plexmod/lib/windows/preplay.py b/script.plexmod/lib/windows/preplay.py new file mode 100644 index 000000000..5b8228b36 --- /dev/null +++ b/script.plexmod/lib/windows/preplay.py @@ -0,0 +1,720 @@ +from __future__ import absolute_import + +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from . import busy +from . import opener +from . import info +from . import videoplayer +from . import playersettings +from . import search +from . import dropdown +from . import windowutils +from . import optionsdialog +from . import preplayutils +from . import pagination + +from plexnet import plexplayer, media + +from lib import util +from lib import metadata + +from lib.util import T + + +VIDEO_RELOAD_KW = dict(includeExtras=1, includeExtrasCount=10, includeChapters=1, includeReviews=1) + + +class RelatedPaginator(pagination.BaseRelatedPaginator): + def getData(self, offset, amount): + return self.parentWindow.video.getRelated(offset=offset, limit=amount) + + +class PrePlayWindow(kodigui.ControlledWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-pre_play.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + THUMB_POSTER_DIM = util.scaleResolution(347, 518) + RELATED_DIM = util.scaleResolution(268, 397) + EXTRA_DIM = util.scaleResolution(329, 185) + ROLES_DIM = util.scaleResolution(334, 334) + PREVIEW_DIM = util.scaleResolution(343, 193) + + ROLES_LIST_ID = 400 + REVIEWS_LIST_ID = 401 + EXTRA_LIST_ID = 402 + RELATED_LIST_ID = 403 + + OPTIONS_GROUP_ID = 200 + PROGRESS_IMAGE_ID = 250 + + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + + INFO_BUTTON_ID = 304 + PLAY_BUTTON_ID = 302 + TRAILER_BUTTON_ID = 303 + SETTINGS_BUTTON_ID = 305 + OPTIONS_BUTTON_ID = 306 + MEDIA_BUTTON_ID = 307 + + PLAYER_STATUS_BUTTON_ID = 204 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.video = kwargs.get('video') + self.parentList = kwargs.get('parent_list') + self.videos = None + self.exitCommand = None + self.trailer = None + self.lastFocusID = None + self.lastNonOptionsFocusID = None + self.initialized = False + self.relatedPaginator = None + + def doClose(self): + self.relatedPaginator = None + kodigui.ControlledWindow.doClose(self) + + def onFirstInit(self): + self.extraListControl = kodigui.ManagedControlList(self, self.EXTRA_LIST_ID, 5) + self.relatedListControl = kodigui.ManagedControlList(self, self.RELATED_LIST_ID, 5) + self.rolesListControl = kodigui.ManagedControlList(self, self.ROLES_LIST_ID, 5) + self.reviewsListControl = kodigui.ManagedControlList(self, self.REVIEWS_LIST_ID, 5) + + self.progressImageControl = self.getControl(self.PROGRESS_IMAGE_ID) + self.setup() + self.initialized = True + + def doAutoPlay(self): + # First reload the video to get all the other info + self.video.reload(checkFiles=1, **VIDEO_RELOAD_KW) + return self.playVideo() + + @busy.dialog() + def onReInit(self): + self.initialized = False + if util.getSetting("slow_connection", False): + self.progressImageControl.setWidth(1) + self.setProperty('remainingTime', T(32914, "Loading")) + self.video.reload(checkFiles=1, fromMediaChoice=self.video.mediaChoice is not None, **VIDEO_RELOAD_KW) + self.refreshInfo(from_reinit=True) + self.initialized = True + + def refreshInfo(self, from_reinit=False): + oldFocusId = self.getFocusId() + + util.setGlobalProperty('hide.resume', '' if self.video.viewOffset.asInt() else '1') + # skip setting background when coming from reinit (other window) if we've focused something other than main + self.setInfo(skip_bg=from_reinit and not (self.PLAY_BUTTON_ID <= oldFocusId <= self.MEDIA_BUTTON_ID)) + self.fillRelated() + xbmc.sleep(100) + + if oldFocusId == self.PLAY_BUTTON_ID: + self.focusPlayButton() + + def onAction(self, action): + try: + controlID = self.getFocusId() + + if not controlID and self.lastFocusID and not action == xbmcgui.ACTION_MOUSE_MOVE: + self.setFocusId(self.lastFocusID) + + if action == xbmcgui.ACTION_CONTEXT_MENU: + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + self.lastNonOptionsFocusID = self.lastFocusID + self.setFocusId(self.OPTIONS_GROUP_ID) + return + else: + if self.lastNonOptionsFocusID: + self.setFocusId(self.lastNonOptionsFocusID) + self.lastNonOptionsFocusID = None + return + + elif action == xbmcgui.ACTION_NAV_BACK: + if (not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format( + self.OPTIONS_GROUP_ID)) or not controlID) and \ + not util.advancedSettings.fastBack: + if self.getProperty('on.extras'): + self.setFocusId(self.OPTIONS_GROUP_ID) + return + + elif action == xbmcgui.ACTION_LAST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + next(self) + elif action == xbmcgui.ACTION_NEXT_ITEM: + self.setFocusId(300) + next(self) + elif action == xbmcgui.ACTION_FIRST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + self.prev() + elif action == xbmcgui.ACTION_PREV_ITEM: + self.setFocusId(300) + self.prev() + + elif action == xbmcgui.ACTION_MOVE_UP and controlID in (self.REVIEWS_LIST_ID, + self.ROLES_LIST_ID, + self.EXTRA_LIST_ID): + self.updateBackgroundFrom(self.video) + + if controlID == self.RELATED_LIST_ID: + if self.relatedPaginator.boundaryHit: + self.relatedPaginator.paginate() + return + elif action in (xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT): + self.updateBackgroundFrom(self.relatedListControl.getSelectedItem().dataSource) + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.EXTRA_LIST_ID: + self.openItem(self.extraListControl) + elif controlID == self.RELATED_LIST_ID: + self.openItem(self.relatedListControl) + elif controlID == self.ROLES_LIST_ID: + self.roleClicked() + elif controlID == self.PLAY_BUTTON_ID: + self.playVideo() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.INFO_BUTTON_ID: + self.infoButtonClicked() + elif controlID == self.SETTINGS_BUTTON_ID: + self.settingsButtonClicked() + elif controlID == self.TRAILER_BUTTON_ID: + self.openItem(item=self.trailer) + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked() + elif controlID == self.MEDIA_BUTTON_ID: + self.mediaButtonClicked() + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def onFocus(self, controlID): + self.lastFocusID = controlID + + if 399 < controlID < 500: + self.setProperty('hub.focus', str(controlID - 400)) + + if controlID == self.RELATED_LIST_ID: + self.updateBackgroundFrom(self.relatedListControl.getSelectedItem().dataSource) + + if xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + ControlGroup(300).HasFocus(0)'): + self.setProperty('on.extras', '') + elif xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + !ControlGroup(300).HasFocus(0)'): + self.setProperty('on.extras', '1') + + def searchButtonClicked(self): + self.processCommand(search.dialog(self, section_id=self.video.getLibrarySectionId() or None)) + + def settingsButtonClicked(self): + if not self.video.mediaChoice: + playerObject = plexplayer.PlexPlayer(self.video) + playerObject.build() + playersettings.showDialog(video=self.video, non_playback=True) + self.setAudioAndSubtitleInfo() + + def infoButtonClicked(self): + opener.handleOpen( + info.InfoWindow, + title=self.video.defaultTitle, + sub_title=self.getProperty('info'), + thumb=self.video.type == 'episode' and self.video.thumb or self.video.defaultThumb, + thumb_fallback='script.plex/thumb_fallbacks/{0}.png'.format(self.video.type == 'episode' and 'show' or 'movie'), + info=self.video.summary, + background=self.getProperty('background'), + is_16x9=self.video.type == 'episode', + video=self.video + ) + + def optionsButtonClicked(self): + options = [] + + inProgress = self.video.viewOffset.asInt() + if not self.video.isWatched or inProgress: + options.append({'key': 'mark_watched', 'display': T(32319, 'Mark Played')}) + if self.video.isWatched or inProgress: + options.append({'key': 'mark_unwatched', 'display': T(32318, 'Mark Unplayed')}) + + options.append(dropdown.SEPARATOR) + + if self.video.type == 'episode': + options.append({'key': 'to_season', 'display': T(32400, 'Go to Season')}) + options.append({'key': 'to_show', 'display': T(32323, 'Go to Show')}) + + if self.video.type in ('episode', 'movie'): + options.append({'key': 'to_section', 'display': T(32324, u'Go to {0}').format(self.video.getLibrarySectionTitle())}) + + if self.video.server.allowsMediaDeletion: + options.append({'key': 'delete', 'display': T(32322, 'Delete')}) + # if xbmc.getCondVisibility('Player.HasAudio') and self.section.TYPE == 'artist': + # options.append({'key': 'add_to_queue', 'display': 'Add To Queue'}) + + # if False: + # options.append({'key': 'add_to_playlist', 'display': 'Add To Playlist'}) + posy = 880 + if not util.getGlobalProperty('hide.resume'): + posy += 106 + if self.getProperty('trailer.button'): + posy += 106 + choice = dropdown.showDropdown(options, (posy, 618), close_direction='left') + if not choice: + return + + if choice['key'] == 'play_next': + xbmc.executebuiltin('PlayerControl(Next)') + elif choice['key'] == 'mark_watched': + self.video.markWatched(**VIDEO_RELOAD_KW) + self.refreshInfo() + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'mark_unwatched': + self.video.markUnwatched(**VIDEO_RELOAD_KW) + self.refreshInfo() + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'to_season': + self.processCommand(opener.open(self.video.parentRatingKey)) + elif choice['key'] == 'to_show': + self.processCommand(opener.open(self.video.grandparentRatingKey)) + elif choice['key'] == 'to_section': + self.goHome(self.video.getLibrarySectionId()) + elif choice['key'] == 'delete': + self.delete() + + def mediaButtonClicked(self): + options = [] + for media in self.video.media: + ind = '' + if self.video.mediaChoice and media.id == self.video.mediaChoice.media.id: + ind = 'script.plex/home/device/check.png' + options.append({'key': media, 'display': media.versionString(), 'indicator': ind}) + choice = dropdown.showDropdown(options, header=T(32450, 'Choose Version'), with_indicator=True) + if not choice: + return False + + for media in self.video.media: + media.set('selected', '') + + self.video.setMediaChoice(choice['key']) + choice['key'].set('selected', 1) + self.setInfo() + + def delete(self): + button = optionsdialog.show( + T(32326, 'Really delete?'), + T(32327, 'Are you sure you really want to delete this media?'), + T(32328, 'Yes'), + T(32329, 'No') + ) + + if button != 0: + return + + if self._delete(): + self.doClose() + else: + util.messageDialog(T(32330, 'Message'), T(32331, 'There was a problem while attempting to delete the media.')) + + @busy.dialog() + def _delete(self): + success = self.video.delete() + util.LOG('Media DELETE: {0} - {1}'.format(self.video, success and 'SUCCESS' or 'FAILED')) + return success + + def roleClicked(self): + mli = self.rolesListControl.getSelectedItem() + if not mli: + return + + sectionRoles = busy.widthDialog(mli.dataSource.sectionRoles, '') + + if not sectionRoles: + util.DEBUG_LOG('No sections found for actor') + return + + if len(sectionRoles) > 1: + x, y = self.getRoleItemDDPosition() + + options = [{'role': r, 'display': r.reasonTitle} for r in sectionRoles] + choice = dropdown.showDropdown(options, (x, y), pos_is_bottom=True, close_direction='bottom') + + if not choice: + return + + role = choice['role'] + else: + role = sectionRoles[0] + + if util.advancedSettings.dialogFlickerFix: + with busy.BusyContext(): + xbmc.sleep(100) + xbmc.sleep(650) + self.processCommand(opener.open(role)) + + def getVideos(self): + if not self.videos: + if self.video.TYPE == 'episode': + self.videos = self.video.show().episodes() + + if not self.videos: + return False + + return True + + def next(self): + if not self._next(): + return + self.setup() + + __next__ = next + + @busy.dialog() + def _next(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.video) + if not mli: + return False + + pos = mli.pos() + 1 + if not self.parentList.positionIsValid(pos): + pos = 0 + + self.video = self.parentList.getListItem(pos).dataSource + else: + if not self.getVideos(): + return False + + if self.video not in self.videos: + return False + + pos = self.videos.index(self.video) + pos += 1 + if pos >= len(self.videos): + pos = 0 + + self.video = self.videos[pos] + + return True + + def prev(self): + if not self._prev(): + return + self.setup() + + @busy.dialog() + def _prev(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.video) + if not mli: + return False + + pos = mli.pos() - 1 + if pos < 0: + pos = self.parentList.size() - 1 + + self.video = self.parentList.getListItem(pos).dataSource + else: + if not self.getVideos(): + return False + + if self.video not in self.videos: + return False + + pos = self.videos.index(self.video) + pos -= 1 + if pos < 0: + pos = len(self.videos) - 1 + + self.video = self.videos[pos] + + return True + + def getRoleItemDDPosition(self): + y = 200 + if xbmc.getCondVisibility('Control.IsVisible(500)'): + y += 360 + if xbmc.getCondVisibility('Control.IsVisible(501)'): + y += 520 + if xbmc.getCondVisibility('!String.IsEmpty(Window.Property(on.extras))'): + y -= 300 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),0) + Control.IsVisible(500)'): + y -= 500 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),1) + Control.IsVisible(501)'): + y -= 500 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),2) + Control.IsVisible(502)'): + y -= 500 + + focus = int(xbmc.getInfoLabel('Container(400).Position')) + + x = ((focus + 1) * 304) - 100 + return x, y + + def playVideo(self): + if not self.video.available(): + util.messageDialog(T(32312, 'Unavailable'), T(32313, 'This item is currently unavailable.')) + return + + resume = False + if self.video.viewOffset.asInt(): + choice = dropdown.showDropdown( + options=[ + {'key': 'resume', 'display': T(32429, 'Resume from {0}').format(util.timeDisplay(self.video.viewOffset.asInt()).lstrip('0').lstrip(':'))}, + {'key': 'play', 'display': T(32317, 'Play from beginning')} + ], + pos=(660, 441), + close_direction='none', + set_dropdown_prop=False, + header=T(32314, 'In Progress') + ) + + if not choice: + return + + if choice['key'] == 'resume': + resume = True + + self.processCommand(videoplayer.play(video=self.video, resume=resume)) + return True + + def openItem(self, control=None, item=None): + if not item: + mli = control.getSelectedItem() + if not mli: + return + item = mli.dataSource + + self.processCommand(opener.open(item)) + + def focusPlayButton(self): + try: + if not self.getFocusId() == self.PLAY_BUTTON_ID: + self.setFocusId(self.PLAY_BUTTON_ID) + except (SystemError, RuntimeError): + self.setFocusId(self.PLAY_BUTTON_ID) + + @busy.dialog() + def setup(self): + self.focusPlayButton() + + util.DEBUG_LOG('PrePlay: Showing video info: {0}'.format(self.video)) + if self.video.type == 'episode': + self.setProperty('preview.yes', '1') + elif self.video.type == 'movie': + self.setProperty('preview.no', '1') + + self.video.reload(checkFiles=1, **VIDEO_RELOAD_KW) + try: + self.relatedPaginator = RelatedPaginator(self.relatedListControl, leaf_count=int(self.video.relatedCount), + parent_window=self) + except ValueError: + raise util.NoDataException + + self.setInfo() + self.setBoolProperty("initialized", True) + hasRoles = self.fillRoles() + hasReviews = self.fillReviews() + hasExtras = self.fillExtras() + self.fillRelated(hasRoles and not hasExtras and not hasReviews) + + def setInfo(self, skip_bg=False): + if not skip_bg: + self.updateBackgroundFrom(self.video) + self.setProperty('title', self.video.title) + self.setProperty('duration', util.durationToText(self.video.duration.asInt())) + self.setProperty('summary', self.video.summary.strip().replace('\t', ' ')) + self.setProperty('unwatched', not self.video.isWatched and '1' or '') + + directors = u' / '.join([d.tag for d in self.video.directors()][:5]) + directorsLabel = len(self.video.directors) > 1 and T(32401, u'DIRECTORS').upper() or T(32383, u'DIRECTOR').upper() + self.setProperty('directors', directors and u'{0} {1}'.format(directorsLabel, directors) or '') + + if self.video.type == 'episode': + self.setProperty('content.rating', '') + self.setProperty('thumb', self.video.defaultThumb.asTranscodedImageURL(*self.THUMB_POSTER_DIM)) + self.setProperty('preview', self.video.thumb.asTranscodedImageURL(*self.PREVIEW_DIM)) + self.setProperty('info', u'{0} {1} {2} {3}'.format(T(32303, 'Season'), self.video.parentIndex, T(32304, 'Episode'), self.video.index)) + self.setProperty('date', util.cleanLeadingZeros(self.video.originallyAvailableAt.asDatetime('%B %d, %Y'))) + + writers = u' / '.join([w.tag for w in self.video.writers()][:5]) + writersLabel = len(self.video.writers) > 1 and T(32403, u'WRITERS').upper() or T(32402, u'WRITER').upper() + self.setProperty('writers', writers and u'{0} {1}'.format(writersLabel, writers) or '') + self.setProperty('related.header', T(32306, 'Related Shows')) + elif self.video.type == 'movie': + self.setProperty('title', self.video.defaultTitle) + self.setProperty('preview', '') + self.setProperty('thumb', self.video.thumb.asTranscodedImageURL(*self.THUMB_POSTER_DIM)) + genres = u' / '.join([g.tag for g in self.video.genres()][:3]) + self.setProperty('info', genres) + self.setProperty('date', self.video.year) + self.setProperty('content.rating', self.video.contentRating.split('/', 1)[-1]) + + cast = u' / '.join([r.tag for r in self.video.roles()][:5]) + castLabel = 'CAST' + self.setProperty('writers', cast and u'{0} {1}'.format(castLabel, cast) or '') + self.setProperty('related.header', T(32404, 'Related Movies')) + + self.setProperty('video.res', self.video.resolutionString()) + self.setProperty('audio.codec', self.video.audioCodecString()) + self.setProperty('video.codec', self.video.videoCodecString()) + self.setProperty('video.rendering', self.video.videoCodecRendering) + self.setProperty('audio.channels', self.video.audioChannelsString(metadata.apiTranslate)) + self.setBoolProperty('media.multiple', len(list(filter(lambda x: x.isAccessible(), self.video.media()))) > 1) + + self.setProperties(('rating.stars', 'rating', 'rating.image', 'rating2', 'rating2.image'), '') + if self.video.userRating: + stars = str(int(round((self.video.userRating.asFloat() / 10) * 5))) + self.setProperty('rating.stars', stars) + # elif self.video.rating: + # stars = str(int(round((self.video.rating.asFloat() / 10) * 5))) + # self.setProperty('rating.stars', stars) + + if self.video.ratingImage: + rating = self.video.rating + audienceRating = self.video.audienceRating + if self.video.ratingImage.startswith('rottentomatoes:'): + rating = '{0}%'.format(int(rating.asFloat() * 10)) + if audienceRating: + audienceRating = '{0}%'.format(int(audienceRating.asFloat() * 10)) + + self.setProperty('rating', rating) + self.setProperty('rating.image', 'script.plex/ratings/{0}.png'.format(self.video.ratingImage.replace('://', '/'))) + if self.video.audienceRatingImage: + self.setProperty('rating2', audienceRating) + self.setProperty('rating2.image', 'script.plex/ratings/{0}.png'.format(self.video.audienceRatingImage.replace('://', '/'))) + else: + self.setProperty('rating', self.video.rating) + + self.setAudioAndSubtitleInfo() + + self.setProperty('unavailable', all(not v.isAccessible() for v in self.video.media()) and '1' or '') + + if self.video.viewOffset.asInt(): + width = self.video.viewOffset.asInt() and (1 + int((self.video.viewOffset.asInt() / self.video.duration.asFloat()) * self.width)) or 1 + self.progressImageControl.setWidth(width) + else: + self.progressImageControl.setWidth(1) + + if self.video.viewOffset.asInt(): + self.setProperty('remainingTime', T(33615, "{time} left").format(time=self.video.remainingTimeString)) + else: + self.setProperty('remainingTime', '') + + def setAudioAndSubtitleInfo(self): + sas = self.video.selectedAudioStream() + self.setProperty('audio', sas and sas.getTitle(metadata.apiTranslate) or T(32309, 'None')) + + sss = self.video.selectedSubtitleStream( + forced_subtitles_override=util.getSetting("forced_subtitles_override", False)) + if sss: + if len(self.video.subtitleStreams) > 1: + self.setProperty( + 'subtitles', u'{0} \u2022 {1} {2}'.format(sss.getTitle(metadata.apiTranslate), len(self.video.subtitleStreams) - 1, T(32307, 'More')) + ) + else: + self.setProperty('subtitles', sss.getTitle(metadata.apiTranslate)) + else: + if self.video.subtitleStreams: + self.setProperty('subtitles', u'{0} \u2022 {1} {2}'.format(T(32309, 'None'), len(self.video.subtitleStreams), T(32308, 'Available'))) + else: + self.setProperty('subtitles', T(32309, u'None')) + + def createListItem(self, obj): + mli = kodigui.ManagedListItem(obj.title or '', thumbnailImage=obj.thumb.asTranscodedImageURL(*self.EXTRA_DIM), data_source=obj) + return mli + + def fillExtras(self, has_prev=False): + items = [] + idx = 0 + + if not self.video.extras: + self.extraListControl.reset() + return False + + for extra in self.video.extras(): + if not self.trailer and extra.extraType.asInt() == media.METADATA_RELATED_TRAILER: + self.trailer = extra + self.setProperty('trailer.button', '1') + continue + + mli = self.createListItem(extra) + if mli: + mli.setProperty('index', str(idx)) + mli.setProperty( + 'thumb.fallback', 'script.plex/thumb_fallbacks/{0}.png'.format(extra.type in ('show', 'season', 'episode') and 'show' or 'movie') + ) + items.append(mli) + idx += 1 + + if not items: + return False + + self.extraListControl.reset() + self.extraListControl.addItems(items) + + self.setProperty('divider.{0}'.format(self.EXTRA_LIST_ID), has_prev and '1' or '') + + return True + + def fillRelated(self, has_prev=False): + if not self.relatedPaginator.leafCount: + self.relatedListControl.reset() + return False + + items = self.relatedPaginator.paginate() + + if not items: + return False + + self.setProperty('divider.{0}'.format(self.RELATED_LIST_ID), has_prev and '1' or '') + + return True + + def fillRoles(self, has_prev=False): + items = [] + idx = 0 + + if not self.video.roles: + self.rolesListControl.reset() + return False + + for role in self.video.roles(): + mli = kodigui.ManagedListItem(role.tag, role.role, thumbnailImage=role.thumb.asTranscodedImageURL(*self.ROLES_DIM), data_source=role) + mli.setProperty('index', str(idx)) + items.append(mli) + idx += 1 + + if not items: + return False + + self.rolesListControl.reset() + self.rolesListControl.addItems(items) + return True + + def fillReviews(self, has_prev=False): + items = [] + idx = 0 + + if not self.video.reviews: + self.reviewsListControl.reset() + return False + + for review in self.video.reviews(): + mli = kodigui.ManagedListItem(review.source, review.tag, thumbnailImage=review.ratingImage()) + mli.setProperty('index', str(idx)) + mli.setProperty('text', review.text) + items.append(mli) + idx += 1 + + if not items: + return False + + self.reviewsListControl.reset() + self.reviewsListControl.addItems(items) + return True diff --git a/script.plexmod/lib/windows/preplayutils.py b/script.plexmod/lib/windows/preplayutils.py new file mode 100644 index 000000000..7d000622f --- /dev/null +++ b/script.plexmod/lib/windows/preplayutils.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import +from . import dropdown + +from lib.util import T + + +def chooseVersion(video): + if len(video.media) > 1: + options = [] + for media in video.media: + ind = '' + if video.mediaChoice and media.id == video.mediaChoice.media.id: + ind = 'script.plex/home/device/check.png' + options.append({'key': media, 'display': media.versionString(), 'indicator': ind}) + choice = dropdown.showDropdown(options, header=T(32450, 'Choose Version'), with_indicator=True) + if not choice: + return False + + for media in video.media: + media.set('selected', '') + + video.setMediaChoice(choice['key']) + choice['key'].set('selected', 1) + + return True + + +def resetVersion(video): + if len(video.media) < 2: + return + + for media in video.media: + media.set('selected', '') + + video.media[0].set('selected', 1) diff --git a/script.plexmod/lib/windows/search.py b/script.plexmod/lib/windows/search.py new file mode 100644 index 000000000..2b4e2998c --- /dev/null +++ b/script.plexmod/lib/windows/search.py @@ -0,0 +1,435 @@ +from __future__ import absolute_import +import time +import threading + +from kodi_six import xbmcgui, xbmc + +from . import kodigui +from . import opener +from . import windowutils + +from lib import util +from lib.kodijsonrpc import rpc + +from plexnet import plexapp + +class SearchDialog(kodigui.BaseDialog, windowutils.UtilMixin): + xmlFile = 'script-plex-search.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + LETTERS = 'abcdefghijklmnopqrstuvwxyz0123456789 ' + SECTION_BUTTONS = { + 901: 'all', + 902: 'movie', + 903: 'show', + 904: 'artist', + 905: 'photo' + } + + EDIT_CONTROL_ID = 650 + BUTTON_A_ID = 1001 + + HUB_POSTER_00 = 2100 + HUB_SQUARE_01 = 2101 + HUB_AR16X9_02 = 2102 + HUB_CIRCLE_03 = 2103 + HUB_POSTER_04 = 2104 + HUB_SQUARE_05 = 2105 + HUB_AR16X9_06 = 2106 + HUB_CIRCLE_07 = 2107 + HUB_POSTER_08 = 2108 + HUB_SQUARE_09 = 2109 + HUB_AR16X9_10 = 2110 + HUB_CIRCLE_11 = 2111 + HUB_POSTER_12 = 2112 + HUB_SQUARE_13 = 2113 + HUB_AR16X9_14 = 2114 + HUB_CIRCLE_15 = 2115 + HUB_POSTER_16 = 2116 + HUB_SQUARE_17 = 2117 + HUB_AR16X9_18 = 2118 + HUB_CIRCLE_19 = 2119 + HUB_POSTER_20 = 2120 + HUB_SQUARE_21 = 2121 + HUB_AR16X9_22 = 2122 + HUB_CIRCLE_23 = 2123 + HUB_POSTER_24 = 2124 + HUB_SQUARE_25 = 2125 + HUB_AR16X9_26 = 2126 + HUB_CIRCLE_27 = 2127 + HUB_POSTER_28 = 2128 + HUB_SQUARE_29 = 2129 + HUB_AR16X9_30 = 2130 + HUB_CIRCLE_31 = 2131 + HUB_POSTER_32 = 2132 + HUB_SQUARE_33 = 2133 + HUB_AR16X9_34 = 2134 + HUB_CIRCLE_35 = 2135 + HUB_POSTER_36 = 2136 + HUB_SQUARE_37 = 2137 + HUB_AR16X9_38 = 2138 + HUB_CIRCLE_39 = 2139 + HUB_POSTER_40 = 2140 + HUB_SQUARE_41 = 2141 + HUB_AR16X9_42 = 2142 + HUB_CIRCLE_43 = 2143 + HUB_POSTER_44 = 2144 + HUB_SQUARE_45 = 2145 + HUB_AR16X9_46 = 2146 + HUB_CIRCLE_47 = 2147 + + HUBMAP = { + 'track': {'type': 'square'}, + 'episode': {'type': 'ar16x9'}, + 'movie': {'type': 'poster'}, + 'show': {'type': 'poster'}, + 'artist': {'type': 'square'}, + 'album': {'type': 'square'}, + 'photoalbum': {'type': 'square'}, + 'photo': {'type': 'square'}, + 'actor': {'type': 'circle'}, + 'director': {'type': 'circle'}, + 'genre': {'type': 'circle'}, + 'playlist': {'type': 'square'}, + } + + SECTION_TYPE_MAP = { + '1': {'thumb': 'script.plex/section_type/movie.png'}, # Movie + '2': {'thumb': 'script.plex/section_type/show.png'}, # Show + '3': {'thumb': 'script.plex/section_type/show.png'}, # Season + '4': {'thumb': 'script.plex/section_type/show.png'}, # Episode + '8': {'thumb': 'script.plex/section_type/music.png'}, # Artist + '9': {'thumb': 'script.plex/section_type/music.png'}, # Album + '10': {'thumb': 'script.plex/section_type/music.png'}, # Track + } + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + windowutils.UtilMixin.__init__(self) + self.parentWindow = kwargs.get('parent_window') + self.sectionID = kwargs.get('section_id') + self.resultsThread = None + self.updateResultsTimeout = 0 + self.isActive = True + self.useKodiKbd = util.getSetting('search_use_kodi_kbd', False) + + def onFirstInit(self): + self.hubControls = ( + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_00, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_01, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_02, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_03, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_04, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_05, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_06, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_07, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_08, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_09, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_10, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_11, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_12, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_13, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_14, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_15, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_16, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_17, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_18, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_19, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_20, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_21, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_22, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_23, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_24, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_25, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_26, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_27, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_28, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_29, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_30, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_31, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_32, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_33, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_34, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_35, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_36, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_37, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_38, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_39, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_40, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_41, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_42, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_43, 5) + }, + { + 'poster': kodigui.ManagedControlList(self, self.HUB_POSTER_44, 5), + 'square': kodigui.ManagedControlList(self, self.HUB_SQUARE_45, 5), + 'ar16x9': kodigui.ManagedControlList(self, self.HUB_AR16X9_46, 5), + 'circle': kodigui.ManagedControlList(self, self.HUB_CIRCLE_47, 5) + }, + ) + + self.edit = kodigui.SafeControlEdit(self.EDIT_CONTROL_ID, 651, self, key_callback=self.updateFromEdit, + grab_focus=True) + self.edit.setCompatibleMode(rpc.Application.GetProperties(properties=["version"])["version"]["major"] < 17) + if self.useKodiKbd: + self.setProperty('hide.kbd', '1') + self.setFocusId(self.EDIT_CONTROL_ID) + xbmc.executebuiltin('Action(Select,{0})'.format(self._winID)) + else: + self.setFocusId(self.BUTTON_A_ID) + self.setProperty('search.section', 'all') + self.updateQuery() + + def onAction(self, action): + try: + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU): + self.isActive = False + except: + util.ERROR() + + kodigui.BaseDialog.onAction(self, action) + + def onClick(self, controlID): + if 1000 < controlID < 1037: + self.letterClicked(controlID) + elif controlID in self.SECTION_BUTTONS: + self.sectionClicked(controlID) + elif controlID == 951: + self.deleteClicked() + elif controlID == 952: + self.letterClicked(1037) + elif controlID == 953: + self.clearClicked() + elif 2099 < controlID < 2200: + self.hubItemClicked(controlID) + + def onFocus(self, controlID): + if 2099 < controlID < 2200: + self.setProperty('hub.focus', str(controlID - 2099)) + + def updateFromEdit(self, actionID, oldVal, newVal): + if actionID == xbmcgui.ACTION_PREVIOUS_MENU: + self.isActive = False + self.doClose() + return + + self.updateQuery() + + def updateQuery(self): + self.updateResults() + + def updateResults(self): + self.updateResultsTimeout = time.time() + 1 + if not self.resultsThread or not self.resultsThread.is_alive(): + self.resultsThread = threading.Thread(target=self._updateResults, name='search.update') + self.resultsThread.start() + + def _updateResults(self): + while time.time() < self.updateResultsTimeout and not util.MONITOR.waitForAbort(0.1): + pass + + self._reallyUpdateResults() + + def _reallyUpdateResults(self): + query = self.edit.getText() + if query: + with self.propertyContext('searching'): + hubs = plexapp.SERVERMANAGER.selectedServer.hubs(count=10, search_query=query, section=self.sectionID) + self.showHubs(hubs) + else: + self.clearHubs() + + def sectionClicked(self, controlID): + section = self.SECTION_BUTTONS[controlID] + old = self.getProperty('search.section') + self.setProperty('search.section', section) + if old != section: + self.updateResults() + + def letterClicked(self, controlID): + letter = self.LETTERS[controlID - 1001] + self.edit.append(letter) + self.updateQuery() + + def deleteClicked(self): + self.edit.delete() + self.updateQuery() + + def clearClicked(self): + self.edit.setText('') + self.updateQuery() + + def hubItemClicked(self, hubControlID): + for controls in self.hubControls: + for control in controls.values(): + if control.controlID == hubControlID: + break + else: + continue + break + else: + return + + mli = control.getSelectedItem() + if not mli: + return + + hubItem = mli.dataSource + if hubItem.TYPE == 'playlist' and not hubItem.exists(): # Workaround for server bug + util.messageDialog('No Access', 'Playlist not accessible by this user.') + util.DEBUG_LOG('Search: Playlist does not exist - probably wrong user') + return + + self.doClose() + try: + command = opener.open(hubItem) + + if not hubItem.exists(): + control.removeManagedItem(mli) + + self.processCommand(command) + finally: + if not self.exitCommand: + self.show() + else: + self.isActive = False + + def createListItem(self, hubItem): + if hubItem.TYPE in ('Genre', 'Director', 'Role'): + if hubItem.TYPE == 'Genre': + thumb = (self.SECTION_TYPE_MAP.get(hubItem.librarySectionType) or {}).get('thumb', '') + mli = kodigui.ManagedListItem(hubItem.tag, hubItem.reasonTitle, thumbnailImage=thumb, data_source=hubItem) + mli.setProperty('thumb.fallback', thumb) + else: + mli = kodigui.ManagedListItem( + hubItem.tag, hubItem.reasonTitle, thumbnailImage=hubItem.get('thumb').asTranscodedImageURL(256, 256), data_source=hubItem + ) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/role.png') + else: + if hubItem.TYPE == 'playlist': + mli = kodigui.ManagedListItem(hubItem.tag, thumbnailImage=hubItem.get('composite').asTranscodedImageURL(256, 256), data_source=hubItem) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/{0}.png'.format(hubItem.playlistType == 'audio' and 'music' or 'movie')) + elif hubItem.TYPE == 'photodirectory': + mli = kodigui.ManagedListItem(hubItem.title, thumbnailImage=hubItem.get('composite').asTranscodedImageURL(256, 256), data_source=hubItem) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/photo.png') + else: + mli = kodigui.ManagedListItem(hubItem.title, thumbnailImage=hubItem.get('thumb').asTranscodedImageURL(256, 256), data_source=hubItem) + if hubItem.TYPE in ('movie', 'clip'): + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/movie.png') + elif hubItem.TYPE in ('artist', 'album', 'track'): + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/music.png') + elif hubItem.TYPE in ('show', 'season', 'episode'): + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/show.png') + elif hubItem.TYPE == 'photo': + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/photo.png') + + return mli + + def showHubs(self, hubs): + self.clearHubs() + self.opaqueBackground(on=False) + + allowed = None + if self.getProperty('search.section') == 'movie': + allowed = ('movie',) + elif self.getProperty('search.section') == 'show': + allowed = ('show', 'season', 'episode') + elif self.getProperty('search.section') == 'artist': + allowed = ('artist', 'album', 'track') + elif self.getProperty('search.section') == 'photo': + allowed = ('photo', 'photodirectory') + + controlID = None + i = 0 + for h in hubs: + if allowed and h.type not in allowed: + continue + + if h.size.asInt() > 0: + self.opaqueBackground() + cid = self.showHub(h, i) + controlID = controlID or cid + i += 1 + + if controlID: + self.setProperty('no.results', '') + else: + self.setProperty('no.results', '1') + + def showHub(self, hub, idx): + util.DEBUG_LOG('Showing search hub: {0} at {1}'.format(hub.type, idx)) + info = self.HUBMAP.get(hub.type) + if not info: + util.DEBUG_LOG('Unhandled hub type: {0}'.format(hub.type)) + return + + itemListControl = self.hubControls[idx][info['type']] + if itemListControl is None: + util.DEBUG_LOG('No control defined') + return + + self.setProperty('hub.{0}'.format(itemListControl.controlID), hub.title) + + items = [] + for hubItem in hub.items: + mli = self.createListItem(hubItem) + items.append(mli) + + itemListControl.reset() + itemListControl.addItems(items) + + return itemListControl.controlID + + def clearHubs(self): + self.opaqueBackground(on=False) + self.setProperty('no.results', '') + for controls in self.hubControls: + for control in controls.values(): + if control: + control.reset() + self.setProperty('hub.focus', '') + + def opaqueBackground(self, on=True): + self.parentWindow.setProperty('search.dialog.hasresults', on and '1' or '') + + def wait(self): + while self.isActive and not util.MONITOR.waitForAbort(0.1): + pass + + +def dialog(parent_window, section_id=None): + parent_window.setProperty('search.dialog.hasresults', '') + with parent_window.propertyContext('search.dialog'): + try: + w = SearchDialog.open(parent_window=parent_window, section_id=section_id) + w.wait() + command = w.exitCommand or '' + del w + return command + finally: + parent_window.setProperty('search.dialog.hasresults', '') diff --git a/script.plexmod/lib/windows/seekdialog.py b/script.plexmod/lib/windows/seekdialog.py new file mode 100644 index 000000000..f5d566743 --- /dev/null +++ b/script.plexmod/lib/windows/seekdialog.py @@ -0,0 +1,2204 @@ +from __future__ import absolute_import +import re +import time +import threading +import math + +from kodi_six import xbmc +from kodi_six import xbmcgui +from collections import OrderedDict + +from . import kodigui +from . import playersettings +from . import dropdown +from . import busy +from plexnet import plexapp + +from lib import util +from plexnet.videosession import VideoSessionInfo, ATTRIBUTE_TYPES as SESSION_ATTRIBUTE_TYPES +from plexnet.exceptions import ServerNotOwned, NotFound +from plexnet.signalsmixin import SignalsMixin + +from lib.kodijsonrpc import builtin + +from lib.util import T +from six.moves import range + +KEY_MOVE_SET = frozenset( + ( + xbmcgui.ACTION_MOVE_LEFT, + xbmcgui.ACTION_MOVE_RIGHT, + xbmcgui.ACTION_MOVE_UP, + xbmcgui.ACTION_MOVE_DOWN + ) +) + +KEY_STEP_SEEK_SET = frozenset( + ( + xbmcgui.ACTION_MOVE_LEFT, + xbmcgui.ACTION_MOVE_RIGHT, + xbmcgui.ACTION_STEP_FORWARD, + xbmcgui.ACTION_STEP_BACK + ) +) + +MARKERS = OrderedDict([ + ("intro", { + "marker": None, + "name": T(32495, 'Skip intro'), + "autoSkipName": T(32800, 'Skipping intro'), + "overrideStartOff": None, + "countdown": None, + + # attrs + "markerAutoSkip": "autoSkipIntro", + "markerAutoSkipped": False, + "markerAutoSkipShownTimer": "_introSkipShownStarted", + "markerSkipBtnTimeout": "skipIntroButtonTimeout", + }), + ("credits", { + "marker": None, + "name": T(32496, 'Skip credits'), + "autoSkipName": T(32801, 'Skipping credits'), + "overrideStartOff": None, + "countdown": None, + + "markerAutoSkip": "autoSkipCredits", + "markerAutoSkipped": False, + "markerAutoSkipShownTimer": "_creditsSkipShownStarted", + "markerSkipBtnTimeout": "skipCreditsButtonTimeout" + }) +]) + +FINAL_MARKER_NEGOFF = 5000 +MARKER_SHOW_NEGOFF = 3000 +MARKER_OFF = 500 +MARKER_CHAPTER_OVERLAP_THRES = 30000 # 30 seconds + + +class SeekDialog(kodigui.BaseDialog): + """ + fixme: This is a convoluted mess. + """ + + xmlFile = 'script-plex-seek_dialog.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + MAIN_BUTTON_ID = 100 + SEEK_IMAGE_ID = 200 + POSITION_IMAGE_ID = 201 + SELECTION_INDICATOR = 202 + SELECTION_INDICATOR_GROUP = 203 + SELECTION_INDICATOR_IMAGE = 204 + SELECTION_INDICATOR_TEXT = 205 + CACHE_IMAGE_ID = 206 + BIF_IMAGE_ID = 300 + SEEK_IMAGE_WIDTH = 1920 + + REPEAT_BUTTON_ID = 401 + SHUFFLE_BUTTON_ID = 402 + SETTINGS_BUTTON_ID = 403 + PREV_BUTTON_ID = 404 + SKIP_BACK_BUTTON_ID = 405 + PLAY_PAUSE_BUTTON_ID = 406 + STOP_BUTTON_ID = 407 + SKIP_FORWARD_BUTTON_ID = 408 + NEXT_BUTTON_ID = 409 + PLAYLIST_BUTTON_ID = 410 + OPTIONS_BUTTON_ID = 411 + SUBTITLE_BUTTON_ID = 412 + + BIG_SEEK_GROUP_ID = 500 + BIG_SEEK_LIST_ID = 501 + + SKIP_MARKER_BUTTON_ID = 791 + NO_OSD_BUTTON_ID = 800 + + BAR_X = 0 + BAR_Y = 921 + BAR_RIGHT = 1920 + BAR_BOTTOM = 969 + + NAVBAR_BTN_SIZE = 60 + + HIDE_DELAY = 4 # This uses the Cron tick so is +/- 1 second accurate + OSD_HIDE_ANIMATION_DURATION = 0.2 + SKIP_STEPS = {"negative": [-10000], "positive": [30000]} + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + + # fixme: heyo, there's a lot of disorder in here. + self.handler = kwargs.get('handler') + self.initialVideoSettings = {} + self.initialAudioStream = None + self.initialSubtitleStream = None + self.bifURL = None + self.baseURL = None + self.hasBif = bool(self.bifURL) + self.baseOffset = 0 + self._duration = 0 + self.offset = 0 + self.selectedOffset = 0 + self.bigSeekOffset = 0 + self.bigSeekChanged = False # attention, with chapters this can become an integer for the True state + self.title = '' + self.title2 = '' + self.fromSeek = 0 + self.initialized = False + self.playlistDialog = None + self.timeout = None + self.autoSeekTimeout = None + self.hasDialog = False + self.lastFocusID = None + self.previousFocusID = None + self.playlistDialogVisible = False + self.forceNextTimeAsChapter = False + self.showChapters = False + self._seeking = False + self._applyingSeek = False + self._seekingWithoutOSD = False + self._delayedSeekThread = None + self._delayedSeekTimeout = 0 + self._osdHideAnimationTimeout = 0 + self._osdHideFast = False + self._hideDelay = self.HIDE_DELAY + self._autoSeekDelay = util.advancedSettings.autoSeekDelay + self._atSkipStep = -1 + self._lastSkipDirection = None + self._forcedLastSkipAmount = None + self._navigatedViaMarkerOrChapter = False + self._lastAction = None + self.lastTimelineResponse = None + self._ignoreInput = False + self._ignoreTick = False + self._abortBufferWait = False + + self._videoBelowOneHour = False + self.timeFmtKodi = util.timeFormatKN + self.waitingForBuffer = False + self.lastSubtitleNavAction = "forward" + self.subtitleButtonLeft = 0 + self.ldTimer = True #util.advancedSettings.lowDriftTimer + self.timeKeeper = None + self.timeKeeperTime = None + self.idleTime = None + self.stopPlaybackOnIdle = util.getSetting('player_stop_on_idle', 0) + self.isDirectPlay = True + self.isTranscoded = False + + # optimize + self._enableMarkerSkip = plexapp.ACCOUNT.hasPlexPass() + self.markers = None + self.chapters = None + self._introSkipShownStarted = None + self._creditsSkipShownStarted = None + self._currentMarker = None + self.skipSteps = self.SKIP_STEPS + self.useAutoSeek = util.advancedSettings.autoSeek + self.useDynamicStepsForTimeline = util.advancedSettings.dynamicTimelineSeek + + self.bingeMode = False + self.autoSkipIntro = False + self.autoSkipCredits = False + self.showIntroSkipEarly = False + + self.skipIntroButtonTimeout = util.advancedSettings.skipIntroButtonTimeout + self.skipCreditsButtonTimeout = util.advancedSettings.skipCreditsButtonTimeout + self.showItemEndsInfo = util.advancedSettings.showMediaEndsInfo + self.showItemEndsLabel = util.advancedSettings.showMediaEndsLabel + + self.player.video.server.on("np:timelineResponse", self.timelineResponseCallback) + + if util.kodiSkipSteps and util.advancedSettings.kodiSkipStepping: + self.skipSteps = {"negative": [], "positive": []} + for step in util.kodiSkipSteps: + key = "negative" if step < 0 else "positive" + self.skipSteps[key].append(step * 1000) + + self.skipSteps["negative"].reverse() + + try: + seconds = int(xbmc.getInfoLabel("Skin.String(SkinHelper.AutoCloseVideoOSD)")) + if seconds > 0: + self._hideDelay = seconds + except ValueError: + pass + + @property + def player(self): + return self.handler.player + + def timelineResponseCallback(self, **kwargs): + response = kwargs.get("response") + self.lastTimelineResponse = response.getBodyXml() + + def resetTimeout(self): + self.timeout = time.time() + self._hideDelay + + def resetAutoSeekTimer(self, value="not_set"): + self.autoSeekTimeout = value if value != "not_set" else time.time() + self._autoSeekDelay + + def resetSeeking(self): + self._seeking = False + self._seekingWithoutOSD = False + self._delayedSeekTimeout = None + self._applyingSeek = False + self.bigSeekChanged = False + self.selectedOffset = None + self.forceNextTimeAsChapter = False + self._navigatedViaMarkerOrChapter = False + self.setProperty('show.chapters', '') + self.setProperty('button.seek', '') + self.setProperty('marker.countdown', '') + self.resetAutoSeekTimer(None) + self.resetSkipSteps() + + def resetMarkerStates(self): + self.setProperty('show.markerSkip', '') + self.setProperty('show.markerSkip_OSDOnly', '') + self.setProperty('marker.autoSkip', '') + self.setProperty('skipMarkerName', '') + + self._introSkipShownStarted = None + self._introAutoSkipped = False + self._creditsSkipShownStarted = None + self._currentMarker = None + self._creditsAutoSkipped = False + self.markers = None + + @property + def DPPlayerOffset(self): + if self.isDirectPlay and self.handler.player and self.handler.player.playerObject: + return self.handler.player.playerObject.startOffset * 1000 + return 0 + + def trueOffset(self): + if self.isDirectPlay: + return self.DPPlayerOffset + self.offset + else: + return self.baseOffset + self.offset + + @property + def markers(self): + # fixme: fix transcoded marker skip + if not self._enableMarkerSkip: + return None + + if not self._markers and hasattr(self.handler.player.video, "markers"): + markers = [] + + for marker in self.handler.player.video.markers: + if marker.type in MARKERS: + m = MARKERS[marker.type].copy() + m["marker"] = marker + m["marker_type"] = marker.type + markers.append(m) + + self._markers = markers + + return self._markers + + @markers.setter + def markers(self, val): + self._markers = val + + def onFirstInit(self): + try: + self._onFirstInit() + except RuntimeError: + util.ERROR(hide_tb=True) + self.started = False + + def _onFirstInit(self): + util.DEBUG_LOG("SeekDialog: onFirstInit") + self.resetTimeout() + self.setProperty('skipMarkerName', T(32495, 'Skip intro')) + self.bigSeekHideTimer = kodigui.PropertyTimer(self._winID, 0.5, 'hide.bigseek') + + if self.handler.playlist: + self.handler.playlist.on('change', self.updateProperties) + + self.seekbarControl = self.getControl(self.SEEK_IMAGE_ID) + self.positionControl = self.getControl(self.POSITION_IMAGE_ID) + self.cacheControl = self.getControl(self.CACHE_IMAGE_ID) + self.bifImageControl = self.getControl(self.BIF_IMAGE_ID) + self.selectionIndicator = self.getControl(self.SELECTION_INDICATOR) + self.selectionIndicatorImage = self.getControl(self.SELECTION_INDICATOR_IMAGE) + self.selectionIndicatorText = self.getControl(self.SELECTION_INDICATOR_TEXT) + self.selectionIndicatorGroup = self.getControl(self.SELECTION_INDICATOR_GROUP) + self.selectionBox = self.getControl(203) + self.bigSeekControl = kodigui.ManagedControlList(self, self.BIG_SEEK_LIST_ID, 12) + self.bigSeekGroupControl = self.getControl(self.BIG_SEEK_GROUP_ID) + self.initialized = True + + showQuickSubs = util.getSetting('subtitle_downloads', False) + showRepeat = util.getSetting('video_show_repeat', False) + showFfwdRwd = util.getSetting('video_show_ffwdrwd', False) + showShuffle = util.getSetting('video_show_shuffle', False) + self.setBoolProperty('nav.quick_subtitles', showQuickSubs) + self.setBoolProperty('nav.repeat', showRepeat) + self.setBoolProperty('nav.ffwdrwd', showFfwdRwd) + self.setBoolProperty('nav.shuffle', showShuffle) + + if showQuickSubs: + self.subtitleButtonLeft += self.NAVBAR_BTN_SIZE * len( + list(x for x in (showRepeat, showFfwdRwd, showShuffle) if not x)) + + self.updateProperties() + self.updateChapters() + self.videoSettingsHaveChanged() + self.update() + + def onReInit(self): + util.DEBUG_LOG("SeekDialog: onReInit") + self.lastTimelineResponse = None + self._lastAction = None + self._ignoreTick = False + self.waitingForBuffer = False + self._abortBufferWait = False + + self.resetTimeout() + self.resetSeeking() + self.updateProperties() + self.updateChapters() + self.videoSettingsHaveChanged() + self.updateProgress() + + def setup(self, duration, meta, offset=0, bif_url=None, title='', title2='', chapters=None, keepMarkerDef=False): + """ + this is called by our handler and occurs earlier than onFirstInit. + """ + util.DEBUG_LOG("SeekDialog: setup, keepMarkerDef={}".format(keepMarkerDef)) + self.title = title + self.title2 = title2 + self.chapters = chapters or [] + self.isDirectPlay = not meta.isTranscoded + self.isTranscoded = not self.isDirectPlay + self.showChapters = util.getUserSetting('show_chapters', True) and ( + bool(chapters) or (util.getUserSetting('virtual_chapters', True) and bool(self.markers))) + self.setProperty('video.title', title) + self.setProperty('is.show', (self.player.video.type == 'episode') and '1' or '') + self.setProperty('has.playlist', self.handler.playlist and '1' or '') + self.setProperty('shuffled', (self.handler.playlist and self.handler.playlist.isShuffled) and '1' or '') + self.setProperty('has.chapters', self.showChapters and '1' or '') + self.setProperty('show.buffer', (util.advancedSettings.playerShowBuffer and self.isDirectPlay) and '1' or '') + + self.killTimeKeeper() + + navPlaylist = util.getSetting('video_show_playlist', 'eponly') + self.setBoolProperty('nav.playlist', (navPlaylist == "eponly" and self.player.video.type == 'episode') or + navPlaylist == "always") + + if not self.getProperty('nav.playlist'): + self.subtitleButtonLeft += self.NAVBAR_BTN_SIZE + + navPrevNext = util.getSetting('video_show_prevnext', 'eponly') + self.setBoolProperty('nav.prevnext', (navPrevNext == "eponly" and self.player.video.type == 'episode') or + navPrevNext == "always") + + if not self.getProperty('nav.prevnext'): + self.subtitleButtonLeft += self.NAVBAR_BTN_SIZE + + try: + if self.player.video.type == 'episode': + pbs = self.player.video.playbackSettings + util.DEBUG_LOG("Playback settings for {}: {}".format(self.player.video.ratingKey, pbs)) + + self.bingeMode = pbs.binge_mode + self.handler.inBingeMode = self.bingeMode + + # don't auto skip intro when on binge mode on the first episode of a season + firstEp = self.player.video.index == '1' + + if self.isDirectPlay or util.getUserSetting('auto_skip_in_transcode', True): + self.autoSkipIntro = (self.bingeMode and not firstEp) or pbs.auto_skip_intro + self.autoSkipCredits = self.bingeMode or pbs.auto_skip_credits + + self.showIntroSkipEarly = self.bingeMode or pbs.show_intro_skip_early + + # in transcoded scenarios, when seeking, keep previous marker states, as the video restarts + if not keepMarkerDef: + self.resetMarkerStates() + except IndexError: + self.doClose(delete=True) + raise util.NoDataException + self.baseOffset = offset + self.offset = 0 + self.idleTime = None + self.lastSubtitleNavAction = "forward" + self._duration = duration + self._videoBelowOneHour = duration / 3600000 < 1 + if self._videoBelowOneHour: + self.timeFmtKodi = self.timeFmtKodi.replace("hh:", "") + self._ignoreTick = False + self._ignoreInput = False + if not self.showChapters: + self.bifURL = bif_url + self.hasBif = bool(self.bifURL) + + if self.hasBif: + self.baseURL = re.sub('/\d+\?', '/{0}?', self.bifURL) + self.update() + + def update(self, offset=None, from_seek=False): + if from_seek: + self.fromSeek = time.time() + else: + if time.time() - self.fromSeek > 0.5: + self.fromSeek = 0 + + if offset is not None: + self.offset = offset + self.selectedOffset = self.trueOffset() + + self.updateProgress() + + def onAction(self, action): + if xbmc.getCondVisibility('Window.IsActive(selectdialog)'): + if self.doKodiSelectDialogHack(action): + return + + try: + self.resetTimeout() + + controlID = self.getFocusId() + self.idleTime = None + + lastAction = self._lastAction + self._lastAction = currentAction = (action.getId(), controlID) + + cancelActions = (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_STOP) + + if not self._ignoreInput: + if action.getId() in KEY_MOVE_SET: + self.setProperty('mouse.mode', '') + if not controlID: + self.setBigSeekShift() + self.setFocusId(400) + return + elif action == xbmcgui.ACTION_MOUSE_MOVE: + self.setProperty('mouse.mode', '1') + + if controlID in (self.MAIN_BUTTON_ID, self.NO_OSD_BUTTON_ID): + if action == xbmcgui.ACTION_MOUSE_LEFT_CLICK: + if self.getProperty('mouse.mode') != '1': + self.setProperty('mouse.mode', '1') + + self.seekMouse(action, without_osd=controlID == self.NO_OSD_BUTTON_ID) + return + elif action == xbmcgui.ACTION_MOUSE_MOVE: + self.seekMouse(action, without_osd=controlID == self.NO_OSD_BUTTON_ID, preview=True) + return + + if action in (xbmcgui.ACTION_PAUSE, xbmcgui.ACTION_PLAYER_PLAY, xbmcgui.ACTION_PLAYER_PLAYPAUSE) and \ + self.player.playState == self.player.STATE_PLAYING: + self.hideOSD() + + passThroughMain = False + if controlID == self.SKIP_MARKER_BUTTON_ID: + if action == xbmcgui.ACTION_SELECT_ITEM: + markerDef = self._currentMarker + if markerDef["marker"]: + marker = markerDef["marker"] + final = getattr(marker, "final", False) + + markerOff = 0 + if marker.type == "credits" and final: + # offset final marker seek so we can trigger postPlay + markerOff = FINAL_MARKER_NEGOFF + + util.DEBUG_LOG('MarkerSkip: Skipping marker {}'.format(markerDef["marker"])) + self.setProperty('show.markerSkip', '') + self.setProperty('show.markerSkip_OSDOnly', '') + self.doSeek(math.ceil(float(marker.endTimeOffset)) - markerOff) + self.hideOSD(skipMarkerFocus=True) + + if marker.type == "credits" and not final: + # non-final marker + setattr(self, markerDef["markerAutoSkipShownTimer"], None) + self.resetAutoSeekTimer(None) + + return + elif action == xbmcgui.ACTION_MOVE_DOWN: + self.setProperty('show.markerSkip_OSDOnly', '1') + self.showOSD() + elif action in (xbmcgui.ACTION_MOVE_RIGHT, xbmcgui.ACTION_STEP_FORWARD, xbmcgui.ACTION_MOVE_LEFT, + xbmcgui.ACTION_STEP_BACK): + # allow no-OSD-seeking with intro skip button shown + passThroughMain = True + elif action == xbmcgui.ACTION_MOVE_UP and self.osdVisible() and self.showChapters: + self.setProperty('show.chapters', '1') + self.setFocusId(self.BIG_SEEK_LIST_ID) + return + + if controlID == self.MAIN_BUTTON_ID: + # we're seeking from the timeline with the OSD open - do an actual timeline seek + if not self._seeking: + self.selectedOffset = self.trueOffset() + + if action in (xbmcgui.ACTION_MOVE_RIGHT, xbmcgui.ACTION_STEP_FORWARD): + if self.useDynamicStepsForTimeline: + return self.skipForward() + return self.seekByOffset(10000, auto_seek=self.useAutoSeek) + + elif action in (xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_STEP_BACK): + if self.useDynamicStepsForTimeline: + return self.skipBack() + return self.seekByOffset(-10000, auto_seek=self.useAutoSeek) + + elif action == xbmcgui.ACTION_MOVE_UP: + if self.getProperty('show.markerSkip') or self.getProperty('show.markerSkip_OSDOnly'): + # pressed up on player controls, then up on MAIN BUTTON; focus marker button + if currentAction == lastAction: + self.setFocusId(self.SKIP_MARKER_BUTTON_ID) + return + elif self.showChapters: + self.setProperty('show.chapters', '1') + + elif action == xbmcgui.ACTION_MOVE_DOWN: + if self.previousFocusID == self.BIG_SEEK_LIST_ID and ( + self.getProperty('show.markerSkip') or self.getProperty('show.markerSkip_OSDOnly')): + self.setFocusId(self.SKIP_MARKER_BUTTON_ID) + self.setProperty('show.chapters', '') + + self.updateBigSeek() + + # elif action == xbmcgui.ACTION_MOVE_UP: + # self.seekForward(60000) + # elif action == xbmcgui.ACTION_MOVE_DOWN: + # self.seekBack(60000) + + # don't auto-apply the currently selected seek when pressing down + elif controlID == self.PLAY_PAUSE_BUTTON_ID and self.previousFocusID == self.MAIN_BUTTON_ID \ + and action == xbmcgui.ACTION_MOVE_DOWN: + self.resetSeeking() + + elif controlID == self.NO_OSD_BUTTON_ID or passThroughMain: + if action in (xbmcgui.ACTION_MOVE_RIGHT, xbmcgui.ACTION_MOVE_LEFT): + # we're seeking from the timeline, with the OSD closed; act as we're skipping + if not self._seeking: + self.selectedOffset = self.trueOffset() + + if action == xbmcgui.ACTION_MOVE_RIGHT: + self.skipForward(without_osd=True) + + else: + self.skipBack(without_osd=True) + if action in (xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_MOVE_DOWN): + # we're seeking from the timeline, with the OSD closed; act as we're skipping + if not self._seeking: + self.selectedOffset = self.trueOffset() + + if self.skipChapter(forward=(action == xbmcgui.ACTION_MOVE_UP), without_osd=True): + return + + if action in ( + xbmcgui.ACTION_MOVE_UP, + xbmcgui.ACTION_MOVE_DOWN, + xbmcgui.ACTION_BIG_STEP_FORWARD, + xbmcgui.ACTION_BIG_STEP_BACK + ) and not self._seekingWithoutOSD: + self.selectedOffset = self.trueOffset() + self.setBigSeekShift() + self.updateProgress() + self.showOSD() + + elif action.getButtonCode() == 61519: + if self.getProperty('show.PPI'): + self.hidePPIDialog() + else: + self.showPPIDialog() + return + + elif controlID == self.BIG_SEEK_LIST_ID: + if action in (xbmcgui.ACTION_MOVE_RIGHT, xbmcgui.ACTION_BIG_STEP_FORWARD): + return self.updateBigSeek(changed=True) + elif action in (xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_BIG_STEP_BACK): + return self.updateBigSeek(changed=True) + + elif action == xbmcgui.ACTION_MOVE_DOWN: + if self.getProperty('show.markerSkip'): + self.setProperty('show.chapters', '') + self.setFocusId(self.SKIP_MARKER_BUTTON_ID) + + if action.getButtonCode() == 61516: + self.cycleSubtitles() + elif action.getButtonCode() == 61524: + self.toggleSubtitles() + elif action.getButtonCode() == 323714: + # Alt-left + builtin.PlayerControl('tempodown') + elif action.getButtonCode() == 323715: + # Alt-right + builtin.PlayerControl('tempoup') + elif action == xbmcgui.ACTION_NEXT_ITEM: + self.handler.ignoreTimelines = True + self._ignoreTick = True + self.killTimeKeeper() + self.handler.next() + elif action == xbmcgui.ACTION_PREV_ITEM: + self.handler.ignoreTimelines = True + self._ignoreTick = True + self.killTimeKeeper() + self.handler.prev() + + if action in cancelActions + (xbmcgui.ACTION_SELECT_ITEM,): + if self.getProperty('show.PPI') and action in cancelActions: + self.hidePPIDialog() + self.hideOSD() + return + + # immediate marker timer actions + if self.countingDownMarker and \ + (self.getProperty('show.markerSkip') or self.getProperty('show.markerSkip_OSDOnly')): + + if controlID != self.BIG_SEEK_LIST_ID and \ + (util.advancedSettings.skipMarkerTimerCancel + or util.advancedSettings.skipMarkerTimerImmediate): + if util.advancedSettings.skipMarkerTimerCancel and \ + action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): + self.displayMarkers(cancelTimer=True) + + elif util.advancedSettings.skipMarkerTimerImmediate \ + and action == xbmcgui.ACTION_SELECT_ITEM: + self.displayMarkers(immediate=True) + self.hideOSD(skipMarkerFocus=True) + return + + if action in cancelActions: + if self.waitingForBuffer: + self._abortBufferWait = True + self.waitingForBuffer = False + return + + if self._seeking and not self._ignoreInput: + self.resetSeeking() + self.updateCurrent() + self.updateProgress() + if self.osdVisible(): + self.hideOSD() + return + + if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): + if self._osdHideAnimationTimeout: + if self._osdHideAnimationTimeout >= time.time(): + return + else: + self._osdHideAnimationTimeout = None + + if self.osdVisible(): + self.hideOSD() + else: + self.stop() + return + except: + util.ERROR() + + kodigui.BaseDialog.onAction(self, action) + + def doKodiSelectDialogHack(self, action): + command = { + xbmcgui.ACTION_MOVE_UP: "Up", + xbmcgui.ACTION_MOVE_DOWN: "Down", + xbmcgui.ACTION_MOVE_LEFT: "Right", # Not sure if these are actually reversed or something else is up here + xbmcgui.ACTION_MOVE_RIGHT: "Left", + xbmcgui.ACTION_SELECT_ITEM: "Select", + xbmcgui.ACTION_PREVIOUS_MENU: "Back", + xbmcgui.ACTION_NAV_BACK: "Back" + }.get(action.getId()) + + if command is not None: + xbmc.executebuiltin('Action({0},selectdialog)'.format(command)) + return True + + return False + + def onFocus(self, controlID): + lastFocusID = self.lastFocusID + self.previousFocusID = self.lastFocusID + self.lastFocusID = controlID + if controlID == self.MAIN_BUTTON_ID: + # when seeking via ENTER/CLICK on chapters, coming directly from bigSeekSelected, don't assume we've + # already seeked. bigSeekSelected sets self.selectedOffset + if not self.showChapters: + self.selectedOffset = self.trueOffset() + + if lastFocusID == self.BIG_SEEK_LIST_ID and self.bigSeekChanged: + self.updateBigSeek(changed=True) + + # in case of chapter mode, bigSeekChanged holds our chapter's offset + offset = self.bigSeekChanged if self.showChapters else self.selectedOffset + self.updateProgress(set_to_current=False, offset=offset) + + # immediately seek bigSeek after click + self._performSeek(offset=offset) + self.hideOSD(skipMarkerFocus=True) + + else: + self.setBigSeekShift() + self.updateProgress() + + elif controlID == self.BIG_SEEK_LIST_ID: + self.setBigSeekShift() + self.updateBigSeek(changed=False) + + elif xbmc.getCondVisibility('ControlGroup(400).HasFocus(0)'): + self.selectedOffset = self.trueOffset() + self.updateProgress() + + def onClick(self, controlID): + if self._ignoreInput: + return + + if controlID in (self.MAIN_BUTTON_ID, self.NO_OSD_BUTTON_ID): + # only react to click events on our main areas if we're not in mouse mode, otherwise mouse seeking is + # handled by onAction + if self.getProperty('mouse.mode') != '1': + if controlID == self.MAIN_BUTTON_ID: + self.resetAutoSeekTimer(None) + self.doSeek() + elif controlID == self.NO_OSD_BUTTON_ID: + if not self._seeking: + # we might be reacting to an immediate marker skip while showing a marker with timeout; + # in that case, don't show the OSD + if not self._currentMarker or not util.advancedSettings.skipMarkerTimerImmediate or \ + self._currentMarker["countdown"] is None: + self.showOSD() + else: + # currently seeking without the OSD, apply the seek + self.doSeek() + elif controlID == self.PLAY_PAUSE_BUTTON_ID \ + and self.player.playState == self.player.STATE_PLAYING \ + and self.osdVisible(): + self.hideOSD() + elif controlID == self.STOP_BUTTON_ID: + self.stop() + elif controlID == self.SETTINGS_BUTTON_ID: + self.handleDialog(self.showSettings) + elif controlID == self.REPEAT_BUTTON_ID: + self.repeatButtonClicked() + elif controlID == self.SHUFFLE_BUTTON_ID: + self.shuffleButtonClicked() + elif controlID == self.PREV_BUTTON_ID: + self.handler.ignoreTimelines = True + self._ignoreTick = True + self.handler.prev() + elif controlID == self.NEXT_BUTTON_ID: + if not self.handler.queuingNext: + self.handler.ignoreTimelines = True + self.handler.queuingNext = True + self._ignoreTick = True + self._ignoreInput = True + self.killTimeKeeper() + next(self.handler) + return + elif controlID == self.PLAYLIST_BUTTON_ID: + self.showPlaylistDialog() + elif controlID == self.OPTIONS_BUTTON_ID: + self.handleDialog(self.optionsButtonClicked) + elif controlID == self.SUBTITLE_BUTTON_ID: + self.handleDialog(self.subtitleButtonClicked) + elif controlID == self.BIG_SEEK_LIST_ID: + self.bigSeekSelected() + elif controlID == self.SKIP_BACK_BUTTON_ID: + self.skipBack() + elif controlID == self.SKIP_FORWARD_BUTTON_ID: + self.skipForward() + + def stop(self): + self._ignoreTick = True + self.doClose() + # self.handler.onSeekAborted() + if self.bingeMode: + self.handler.stoppedInBingeMode = True + self.handler.player.stop() + + def doClose(self, delete=False): + if self.handler.playlist: + self.handler.playlist.off('change', self.updateProperties) + + try: + if self.playlistDialog: + self.playlistDialog.doClose() + if delete: + del self.playlistDialog + self.playlistDialog = None + util.garbageCollect() + + self.killTimeKeeper() + finally: + kodigui.BaseDialog.doClose(self) + + def showPPIDialog(self): + for attrib in SESSION_ATTRIBUTE_TYPES.values(): + self.setProperty('ppi.%s' % attrib.label, "") + + self.setProperty('show.PPI', '1') + self.setProperty('ppi.Status', 'Loading ...') + + def getVideoSession(currentVideo): + return currentVideo.server.findVideoSession(currentVideo.settings.getGlobal("clientIdentifier"), + currentVideo.ratingKey) + + if util.KODI_BUILD_NUMBER < 2090821: + try: + cache = int(xbmc.getInfoLabel('Player.ProgressCache')) - int(xbmc.getInfoLabel('Player.Progress')) + self.setProperty('ppi.Buffered', str(cache)) + except: + pass + + while not self.player.started: + util.MONITOR.waitForAbort(0.1) + + info = None + currentVideo = self.player.video + try: + videoSession = None + elapsed = 0 + while not videoSession: + if elapsed > 10: + raise NotFound + + videoSession = getVideoSession(currentVideo) + if videoSession: + break + + util.MONITOR.waitForAbort(1) + elapsed += 1 + + # fill attributes + info = VideoSessionInfo(videoSession, currentVideo) + + except ServerNotOwned: + # timeline response data fallback + elapsed = 0 + try: + while not self.lastTimelineResponse: + if elapsed > 10: + raise NotFound + + util.MONITOR.waitForAbort(0.1) + elapsed += 0.1 + + info = VideoSessionInfo(None, currentVideo, incompleteSessionData=self.lastTimelineResponse) + except NotFound: + self.setProperty('ppi.Status', 'Info not available (data not found)') + + except: + util.ERROR() + + except NotFound: + self.setProperty('ppi.Status', 'Info not available (session not found)') + + except: + util.ERROR() + + if info: + self.setProperty('ppi.Status', '') + for attrib in info.attributes.values(): + self.setProperty('ppi.%s' % attrib.label, attrib.value) + + def hidePPIDialog(self): + self.setProperty('show.PPI', '') + + def resetSkipSteps(self): + self._forcedLastSkipAmount = None + self._atSkipStep = -1 + self._lastSkipDirection = None + + def determineSkipStep(self, direction): + stepCount = len(self.skipSteps[direction]) + + # shortcut for simple skipping + if stepCount == 1: + return self.skipSteps[direction][0] + + use_direction = direction + + # kodi-style skip steps + + # when the direction changes, we either use the skip steps of the other direction, or walk backwards in the + # current skip step list + if self._lastSkipDirection != direction: + if self._atSkipStep == -1 or self._lastSkipDirection is None: + self._atSkipStep = 0 + self._lastSkipDirection = direction + self._forcedLastSkipAmount = None + step = self.skipSteps[use_direction][0] + + else: + # we're reversing the current direction + use_direction = self._lastSkipDirection + + # use the inverse value of the current skip step + step = self.skipSteps[use_direction][min(self._atSkipStep, len(self.skipSteps[use_direction]) - 1)] * -1 + + # we've hit a boundary, reverse the difference of the last skip step in relation to the boundary + if self._forcedLastSkipAmount is not None: + step = self._forcedLastSkipAmount * -1 + self._forcedLastSkipAmount = None + + # walk back one step + self._atSkipStep -= 1 + else: + # no reversal of any kind was requested and we've not hit any boundary, use the next skip step + if self._forcedLastSkipAmount is None: + self._atSkipStep += 1 + step = self.skipSteps[use_direction][min(self._atSkipStep, stepCount - 1)] + + else: + # we've hit a timeline boundary and haven't reversed yet. Don't do any further skipping + return + + return step + + def skipChapter(self, forward=True, without_osd=False): + lastSelectedOffset = self.selectedOffset + util.DEBUG_LOG('chapter skipping from {0} with forward {1}'.format(lastSelectedOffset, forward)) + if forward: + nextChapters = [c for c in self.chapters if c.startTime() > lastSelectedOffset] + util.DEBUG_LOG('Found {0} chapters among {1}'.format(len(nextChapters), len(self.chapters))) + if len(nextChapters) == 0: + return False + chapter = nextChapters[0] + else: + startTimeLimit = lastSelectedOffset - 2000 + if startTimeLimit < 0: + startTimeLimit = 0 + lastChapters = [c for c in self.chapters if c.startTime() <= startTimeLimit] + util.DEBUG_LOG('Found {0} chapters among {1}'.format(len(lastChapters), len(self.chapters))) + if len(lastChapters) == 0: + return False + chapter = lastChapters[-1] + + if chapter.tag: + util.DEBUG_LOG('Skipping to chapter: {}'.format(chapter.tag)) + self.forceNextTimeAsChapter = chapter.tag + + util.DEBUG_LOG('New start time is {0}'.format(chapter.startTime())) + self.skipByOffset(chapter.startTime() - lastSelectedOffset, without_osd=without_osd) + return True + + def skipForward(self, without_osd=False): + self.skipByStep("positive", without_osd) + + def skipBack(self, without_osd=False): + self.skipByStep("negative", without_osd) + + def skipByStep(self, direction="positive", without_osd=False): + step = self.determineSkipStep(direction) + self.skipByOffset(step, without_osd) + + def skipByOffset(self, offset, without_osd=False): + if self.countingDownMarker: + self.displayMarkers(cancelTimer=True) + + if offset is not None: + if not self.seekByOffset(offset, without_osd=without_osd): + return + + if self.useAutoSeek: + self.delayedSeek() + else: + self.setProperty('button.seek', '1') + + def delayedSeek(self): + self.setProperty('button.seek', '1') + delay = self._autoSeekDelay + + if delay > 0: + self._delayedSeekTimeout = time.time() + delay + + if not self._delayedSeekThread or not self._delayedSeekThread.is_alive(): + self._delayedSeekThread = threading.Thread(target=self._delayedSeek) + self._delayedSeekThread.start() + else: + # Do seek now + self._performSeek() + self.resetSeeking() + + def _delayedSeek(self): + try: + while not util.MONITOR.waitForAbort(0.1): + if not self._delayedSeekTimeout or time.time() > self._delayedSeekTimeout: + break + + if not util.MONITOR.abortRequested() and self._delayedSeekTimeout is not None: + self._performSeek() + except: + util.ERROR() + + def _performSeek(self, offset=None): + self._lastSkipDirection = None + self._forcedLastSkipAmount = None + self.doSeek(offset=offset) + + def handleDialog(self, func): + self.hasDialog = True + + try: + func() + finally: + self.resetTimeout() + self.hasDialog = False + + def videoSettingsHaveChanged(self): + changed = False + if ( + self.player.video.settings.prefOverrides != self.initialVideoSettings or + self.player.video.selectedAudioStream() != self.initialAudioStream + ): + self.initialVideoSettings = dict(self.player.video.settings.prefOverrides) + self.initialAudioStream = self.player.video.selectedAudioStream() + changed = True + + sss = self.player.video.selectedSubtitleStream() + if sss != self.initialSubtitleStream: + self.initialSubtitleStream = sss + if changed or self.isTranscoded: + return True + else: + return 'SUBTITLE' + + return changed + + def repeatButtonClicked(self): + pl = self.handler.playlist + + if pl: + if pl.isRepeatOne: + pl.setRepeat(False, one=False) + self.updateProperties() + elif pl.isRepeat: + pl.setRepeat(False, one=True) + pl.refresh(force=True) + else: + pl.setRepeat(True) + pl.refresh(force=True) + else: + xbmc.executebuiltin('PlayerControl(Repeat)') + + def shuffleButtonClicked(self): + if self.handler.playlist: + self.handler.playlist.setShuffle() + + def optionsButtonClicked(self): # Button currently commented out. + pass + # options = [] + + # options.append({'key': 'kodi_video', 'display': 'Video Options'}) + # options.append({'key': 'kodi_audio', 'display': 'Audio Options'}) + + # choice = dropdown.showDropdown(options, (1360, 1060), close_direction='down', pos_is_bottom=True, close_on_playback_ended=True) + + # if not choice: + # return + + # if choice['key'] == 'kodi_video': + # xbmc.executebuiltin('ActivateWindow(OSDVideoSettings)') + # elif choice['key'] == 'kodi_audio': + # xbmc.executebuiltin('ActivateWindow(OSDAudioSettings)') + + def subtitleButtonClicked(self): + options = [] + + options.append({'key': 'download', 'display': T(32405, 'Download Subtitles')}) + + # select "enable" by default + selectIndex = 1 + if self.lastSubtitleNavAction == "download": + selectIndex = 0 + + if self.player.video.hasSubtitles: + if self.player.video.hasSubtitle: + options.append({'key': 'delay', 'display': T(32406, 'Subtitle Delay')}) + + # select "disable" if we only have one subtitle + selectIndex = 2 + if self.lastSubtitleNavAction == "delay": + selectIndex = 1 + elif self.lastSubtitleNavAction == "download": + selectIndex = 0 + + if len(self.player.video.subtitleStreams) > 1: + options.append({'key': 'prev', 'display': T(32930, 'Previous Subtitle')}) + options.append({'key': 'next', 'display': T(32407, 'Next Subtitle')}) + + # select "next subtitle" if we already have subs active + selectIndex = 3 + + # select "prev subtitle" if we've last cycled backwards + if self.lastSubtitleNavAction == "backward": + selectIndex = 2 + elif self.lastSubtitleNavAction == "delay": + selectIndex = 1 + elif self.lastSubtitleNavAction == "download": + selectIndex = 0 + + options.append( + { + 'key': 'enable', + 'display': + xbmc.getCondVisibility('VideoPlayer.SubtitlesEnabled') and self.player.video.hasSubtitle and + T(32408, 'Disable Subtitles') or T(32409, 'Enable Subtitles') + } + ) + + # cheap and inaccurate approach to move the dropdown to the left based on how many buttons the user has hidden + choice = dropdown.showDropdown(options, (1360 - self.subtitleButtonLeft, 1060), close_direction='down', pos_is_bottom=True, + close_on_playback_ended=True, select_index=selectIndex) + + if not choice: + return + + if choice['key'] == 'download': + self.hideOSD() + if self.handler and self.handler.player and self.handler.player.playerObject \ + and util.getSetting('calculate_oshash', False): + meta = self.handler.player.playerObject.metadata + oss_hash = util.getOpenSubtitlesHash(meta.size, meta.streamUrls[0]) + if oss_hash: + util.DEBUG_LOG("OpenSubtitles hash: %s" % oss_hash) + util.setGlobalProperty("current_oshash", oss_hash, base='videoinfo.{0}') + else: + util.setGlobalProperty("current_oshash", '', base='videoinfo.{0}') + self.lastSubtitleNavAction = "download" + builtin.ActivateWindow('SubtitleSearch') + elif choice['key'] == 'delay': + self.hideOSD() + self.lastSubtitleNavAction = "delay" + builtin.Action('SubtitleDelay') + elif choice['key'] == 'next': + self.cycleSubtitles() + self.lastSubtitleNavAction = "forward" + elif choice['key'] == 'prev': + self.cycleSubtitles(forward=False) + self.lastSubtitleNavAction = "backward" + elif choice['key'] == 'enable': + enabled = self.toggleSubtitles() + self.lastSubtitleNavAction = "forward" + + def toggleSubtitles(self): + """ + Used for subtitle toggling from button press or subtitle toggle menu + """ + if xbmc.getCondVisibility('VideoPlayer.SubtitlesEnabled') and self.player.video.hasSubtitle: + self.disableSubtitles() + return False + else: + self.cycleSubtitles() + return True + + def disableSubtitles(self): + self.player.video.disableSubtitles() + self.setSubtitles() + + def cycleSubtitles(self, forward=True): + """ + Selects the first subtitle or the next one + """ + stream = self.player.video.cycleSubtitles(forward=forward) + self.setSubtitles(honor_forced_subtitles_override=False) + util.showNotification(str(stream), time_ms=1500, header=util.T(32396, "Subtitles")) + + def setSubtitles(self, do_sleep=False, honor_forced_subtitles_override=False): + self.handler.setSubtitles(do_sleep=do_sleep, honor_forced_subtitles_override=honor_forced_subtitles_override) + if self.player.video.current_subtitle_is_embedded: + # this is an embedded stream, seek back a second after setting the subtitle due to long standing kodi + # issue: https://github.com/xbmc/xbmc/issues/21086 + util.DEBUG_LOG("Switching embedded subtitle stream, seeking due to Kodi issue #21086") + + # true offset can be 0, which might lead to an infinite loop, seek to 100ms at least. + self.doSeek(max(self.trueOffset() - 100, 100)) + + def showSettings(self): + with self.propertyContext('settings.visible'): + playersettings.showDialog(self.player.video, via_osd=True, parent=self) + + changed = self.videoSettingsHaveChanged() + + if self.player.playState == self.player.STATE_PLAYING: + self._osdHideFast = True + + if changed == 'SUBTITLE': + self.setSubtitles(do_sleep=False) + self.lastSubtitleNavAction = "forward" + + elif changed: + self.doSeek(self.trueOffset(), settings_changed=True) + + def setBigSeekShift(self): + closest = None + if self.selectedOffset is None: + return + + for mli in self.bigSeekControl: + if mli.dataSource > self.selectedOffset: + break + closest = mli + if not closest: + return + + self.bigSeekOffset = self.selectedOffset - closest.dataSource + pxOffset = int(self.bigSeekOffset / float(self.duration) * 1920) + + if not self.showChapters: + self.bigSeekGroupControl.setPosition(-8 + pxOffset, 917) + self.bigSeekControl.selectItem(closest.pos()) + + self._seeking = True + # xbmc.sleep(100) + + def updateBigSeek(self, changed=False): + if changed and not self.showChapters: + self.bigSeekChanged = True + self.selectedOffset = self.bigSeekControl.getSelectedItem().dataSource + self.bigSeekOffset + self.updateProgress(set_to_current=False) + elif self.showChapters: + # when hovering chapters, show its corresponding time on the timeline, but don't act like we're seeking + self.updateProgress(set_to_current=False, offset=self.bigSeekControl.getSelectedItem().dataSource, + onlyTimeIndicator=True) + self.resetSkipSteps() + + def bigSeekSelected(self): + # this gets called when a click action happened on the bigSeek, defer the actual action to onFocus + # by setFocusId(MAIN) + + self.bigSeekChanged = True + if self.showChapters: + self.resetAutoSeekTimer(None) + self._navigatedViaMarkerOrChapter = True + + sel = self.bigSeekControl.getSelectedItem() + if self.bigSeekControl.isLastItem(sel): + self.selectedOffset = sel.dataSource - FINAL_MARKER_NEGOFF + else: + self.selectedOffset = sel.dataSource + MARKER_OFF + + # the onFocus action might take a couple of ms and might be overridden by onAction, store separately + self.bigSeekChanged = self.selectedOffset + + self.setFocusId(self.MAIN_BUTTON_ID) + + def updateProperties(self, **kwargs): + if not self.started: + return + + if self.fromSeek: + self.setFocusId(self.MAIN_BUTTON_ID) + self.fromSeek = 0 + + self.setProperty('has.bif', self.bifURL and '1' or '') + self.setProperty('video.title', self.title) + self.setProperty('video.title2', self.title2) + self.setProperty('is.show', (self.player.video.type == 'episode') and '1' or '') + self.setProperty('media.show_ends', self.showItemEndsInfo and '1' or '') + self.setProperty('time.ends_label', self.showItemEndsLabel and (util.T(32543, 'Ends at')) or '') + + if self.isDirectPlay: + self.setProperty('time.fmt', self.timeFmtKodi) + self.setProperty('time.fmt.ends', util.timeFormatKN.replace(":ss", "")) + + self.setBoolProperty('direct.play', self.isDirectPlay) + + if not self.getProperty('nav.playlist') and self.getProperty('nav.quick_subtitles'): + # offset the subtitle button + self.getControl(self.SUBTITLE_BUTTON_ID).setPosition(30, 0) + + if not self.getProperty('nav.prevnext'): + if self.getProperty('nav.ffwdrwd'): + self.getControl(self.SKIP_BACK_BUTTON_ID).setPosition(30, 0) + + pq = self.handler.playlist + if pq: + self.setProperty('has.playlist', '1') + self.setProperty('pq.isRemote', pq.isRemote and '1' or '') + self.setProperty('pq.hasnext', pq.hasNext() and '1' or '') + self.setProperty('pq.hasprev', pq.hasPrev() and '1' or '') + self.setProperty('pq.repeat', pq.isRepeat and '1' or '') + self.setProperty('pq.repeat.one', pq.isRepeatOne and '1' or '') + self.setProperty('pq.shuffled', pq.isShuffled and '1' or '') + else: + self.setProperties(('pq.isRemote', 'pq.hasnext', 'pq.hasprev', 'pq.repeat', 'pq.shuffled', 'has.playlist'), + '') + + self.updateCurrent() + + def updateChapters(self): + items = [] + + # replace bigSeek with chapters or markers if possible + if self.showChapters: + chaps = [] + chapOffsets = [] + if self.chapters: + self.setProperty('chapters.label', T(33605, 'Video Chapters').upper()) + for index, chapter in enumerate(self.chapters): + thumb = chapter.thumb and chapter.thumb.asTranscodedImageURL( + *PlaylistDialog.LI_AR16X9_THUMB_DIM) or None + # mli = kodigui.ManagedListItem(data_source=chapter.startTime(), + # thumbnailImage=thumb, + # label=chapter.tag or T(33607, 'Chapter {}').format(index + 1)) + # items.append(mli) + st = chapter.startTime() + chapOffsets.append(st) + chaps.append((st, thumb, chapter.tag or T(33607, 'Chapter {}').format(index + 1))) + + # fake chapters by using markers + if util.getSetting('virtual_chapters', True) and self.markers: + if not self.chapters: + self.setProperty('chapters.label', T(33606, 'Virtual Chapters').upper()) + else: + self.setProperty('chapters.label', T(33634, 'Combined Chapters').upper()) + creditsCounter = 0 + preparedMarkers = [] + for markerDef in self.markers: + marker = markerDef["marker"] + if marker: + if markerDef["marker_type"] == "intro": + preparedMarkers.append((int(marker.startTimeOffset), T(33608, "Intro"), False)) + preparedMarkers.append((int(marker.endTimeOffset), T(33610, "Main"), False)) + + elif markerDef["marker_type"] == "credits": + creditsCounter += 1 + if creditsCounter > 1 and getattr(marker, "final", False): + label = T(33635, "Final Credits") + else: + label = T(33609, "Credits") + "{}" + preparedMarkers.append((int(marker.startTimeOffset), label, True)) + + # add staggered virtual markers + preparedMarkers.append((int(self.duration * 0.25), "25 %", False)) + preparedMarkers.append((int(self.duration * 0.50), "50 %", False)) + preparedMarkers.append((int(self.duration * 0.75), "75 %", False)) + + credCnt = 1 + for offset, label, credits in sorted(preparedMarkers): + # filter intersections + skipMarker = False + for cOffset in chapOffsets: + if offset - MARKER_CHAPTER_OVERLAP_THRES <= cOffset <= offset + MARKER_CHAPTER_OVERLAP_THRES: + skipMarker = True + break + + # skip marker if we're overlapping with any chapter + if skipMarker: + continue + + chaps.append((offset, self.handler.player.playerObject.getBifUrl(offset), + label.format(" #{}".format(credCnt) if credits and creditsCounter > 1 else ""))) + + if credits: + credCnt += 1 + + for offset, thumb, label in sorted(chaps): + mli = kodigui.ManagedListItem(data_source=offset, thumbnailImage=thumb, label=label) + items.append(mli) + + else: + div = int(self.duration / 12) + for x in range(12): + offset = div * x + items.append(kodigui.ManagedListItem(data_source=offset)) + + # we might've been reinizialized by the handler and have had markers/chapters before. reset height and + # positioning of the bigSeekControl + self.bigSeekControl.control.setHeight(16) + self.bigSeekControl.control.setPosition(self.bigSeekControl.getX(), 0) + + self.bigSeekControl.reset() + self.bigSeekControl.addItems(items) + + if self.showChapters: + # adjust height and positioning of bigSeekControl to accomodate chapters + self.bigSeekControl.control.setHeight(160) + self.bigSeekControl.control.setPosition(self.bigSeekControl.getX(), -126) + + def updateCurrent(self, update_position_control=True, atOffset=None): + ratio = self.trueOffset() / float(self.duration) + + if update_position_control: + w = int(ratio * self.SEEK_IMAGE_WIDTH) + self.positionControl.setWidth(w) + + # update cache/buffer bar + if util.advancedSettings.playerShowBuffer and self.isDirectPlay and util.KODI_VERSION_MAJOR > 18: + cache_w = int(xbmc.getInfoLabel("Player.ProgressCache")) * self.SEEK_IMAGE_WIDTH // 100 + self.cacheControl.setWidth(cache_w) + + if self.isTranscoded: + to = atOffset if atOffset is not None else self.trueOffset() + self.setProperty('time.current', util.timeDisplay(to, cutHour=self._videoBelowOneHour)) + self.setProperty('time.left', + util.timeDisplay(self.duration - to, cutHour=self._videoBelowOneHour)) + + _fmt = util.timeFormat.replace(":%S", "") + + val = time.strftime(_fmt, time.localtime(time.time() + ((self.duration - to) / 1000))) + if not util.padHour and val[0] == "0" and val[1] != ":": + val = val[1:] + + self.setProperty('time.end', val) + + def doSeek(self, offset=None, settings_changed=False): + self._applyingSeek = True + self._ignoreInput = settings_changed + offset = self.selectedOffset if offset is None else offset + + if self.countingDownMarker: + self.displayMarkers(cancelTimer=True) + + self.resetSkipSteps() + self.updateProgress(offset=offset) + + try: + self.handler.seek(offset, settings_changed=settings_changed) + finally: + self.resetSeeking() + + def seekByOffset(self, offset, auto_seek=False, without_osd=False): + """ + Sets the selected offset and updates the progress bar to visually represent the current seek + :param offset: offset to seek to + :param auto_seek: whether to automatically seek to :offset: after a certain amount of time + :param without_osd: indicates whether this seek was done with or without OSD + :return: + """ + if self.selectedOffset is None: + self.selectedOffset = self.offset + lastSelectedOffset = self.selectedOffset + # If we are seeking forward and already past 5 seconds from end, don't seek at all + if lastSelectedOffset > self.duration - 5000 and offset > 0: + return False + + self._seeking = True + self._seekingWithoutOSD = without_osd + self.selectedOffset += offset + # Don't skip past 5 seconds from end + if self.selectedOffset > self.duration - 5000: + # offset = +100, at = 80000, duration = 80007, realoffset = 2 + self._forcedLastSkipAmount = self.duration - 5000 - lastSelectedOffset + self.selectedOffset = self.duration - 5000 + # Don't skip back past 1 (0 is handled specially so seeking to 0 will not do a seek) + elif self.selectedOffset < 1: + # offset = -100, at = 5, realat = -95, realoffset = 1 - 5 = -4 + self._forcedLastSkipAmount = 1 - lastSelectedOffset + self.selectedOffset = 1 + + self.updateProgress(set_to_current=False) + self.setBigSeekShift() + if auto_seek: + self.resetAutoSeekTimer() + self.bigSeekHideTimer.reset() + return True + + def seekMouse(self, action, without_osd=False, preview=False): + x = self.mouseXTrans(action.getAmount1()) + y = self.mouseYTrans(action.getAmount2()) + if not (self.BAR_Y <= y <= self.BAR_BOTTOM): + return + + if not (self.BAR_X <= x <= self.BAR_RIGHT): + return + + self._seeking = True + self._seekingWithoutOSD = without_osd + + self.selectedOffset = int((x - self.BAR_X) / float(self.SEEK_IMAGE_WIDTH) * self.duration) + if not preview: + self.doSeek() + if not xbmc.getCondVisibility('Window.IsActive(videoosd) | Player.Rewinding | Player.Forwarding'): + self.hideOSD() + else: + self.updateProgress(set_to_current=False) + self.setProperty('button.seek', '1') + + def getCurrentMarkerDef(self, offset=None): + """ + Show intro/credits skip button at current time + """ + + if not self.markers: + return + + off = offset if offset is not None else self.trueOffset() + + for markerDef in self.markers: + marker = markerDef["marker"] + if marker: + startTimeOffset = int(marker.startTimeOffset) + + # show intro skip early? (only if intro is during the first X minutes) + if self.showIntroSkipEarly and markerDef["marker_type"] == "intro" and \ + startTimeOffset <= util.advancedSettings.skipIntroButtonShowEarlyThreshold1 * 1000: + startTimeOffset = 0 + markerDef["overrideStartOff"] = 0 + + markerEndNegoff = FINAL_MARKER_NEGOFF if getattr(markerDef["marker"], "final", False) else 0 + + if startTimeOffset - MARKER_SHOW_NEGOFF <= off < int(marker.endTimeOffset) - markerEndNegoff: + + return markerDef + + + @property + def duration(self): + try: + return self._duration or int(self.handler.player.getTotalTime() * 1000) + except RuntimeError: # Not playing + return 1 + + def updateProgress(self, set_to_current=True, offset=None, onlyTimeIndicator=False): + """ + Updates the progress bars (seek and position) and the currently-selected-time-label for the current position or + seek state on the timeline. + :param set_to_current: if True, sets both the position bar and the seek bar to the currently selected position, + otherwise we're in seek mode, whereas one of both bars move relatively to the currently + selected position depending on the direction of the seek + :return: None + """ + if not self.initialized: + return + + offset = offset if offset is not None else \ + self.selectedOffset if self.selectedOffset is not None else self.trueOffset() + ratio = offset / float(self.duration) + w = int(ratio * self.SEEK_IMAGE_WIDTH) + + current_w = int(self.offset / float(self.duration) * self.SEEK_IMAGE_WIDTH) + + bifx = (w - int(ratio * 324)) + self.BAR_X + # bifx = w + self.selectionIndicator.setPosition(w, 896) + if w < 51: + self.selectionBox.setPosition(-50 + (50 - w), 0) + elif w > 1869: + self.selectionBox.setPosition(-100 + (1920 - w), 0) + else: + self.selectionBox.setPosition(-50, 0) + + if self.forceNextTimeAsChapter: + self.setProperty('time.selection', self.forceNextTimeAsChapter) + + # fixme: might be superfluous + self.selectionIndicatorImage.setWidth(self.selectionIndicatorText.getWidth()) + self.forceNextTimeAsChapter = False + else: + self.setProperty('time.selection', util.simplifiedTimeDisplay(offset)) + self.selectionIndicatorImage.setWidth(101) + + if onlyTimeIndicator: + return + + if self.hasBif: + self.setProperty('bif.image', self.handler.player.playerObject.getBifUrl(offset)) + self.bifImageControl.setPosition(bifx, 752) + + self.seekbarControl.setPosition(0, self.seekbarControl.getPosition()[1]) + if set_to_current: + self.seekbarControl.setWidth(w) + self.positionControl.setWidth(w) + else: + # we're seeking + if not self.selectedOffset: + return + + # current seek position below current offset? set the position bar's width to the current position of the + # seek and the seek bar to the current position of the video, to visually indicate the backwards-seeking + if self.selectedOffset < self.offset: + self.positionControl.setWidth(current_w) + self.seekbarControl.setWidth(w) + + # current seek position ahead of current offset? set the position bar's width to the current position of the + # video and the seek bar to the current position of the seek, to visually indicate the forwards-seeking + elif self.selectedOffset > self.offset: + self.seekbarControl.setPosition(current_w, self.seekbarControl.getPosition()[1]) + self.seekbarControl.setWidth(w - current_w) + # we may have "shortened" the width before, by seeking negatively, reset the position bar's width to + # the current video's position if that's the case + if self.positionControl.getWidth() < current_w: + self.positionControl.setWidth(current_w) + + else: + self.seekbarControl.setWidth(w) + self.positionControl.setWidth(w) + + def waitForBuffer(self): + # current filesize in bytes + size = float(self.handler.player.video.mediaChoice.part.size) + + # current buffer fill percentage + currentBufferPerc = int(xbmc.getInfoLabel("Player.ProgressCache")) - int(xbmc.getInfoLabel("Player.Progress")) + + # configured buffer size + bufferBytes = util.kcm.memorySize * 1024 * 1024 + + # wait for the full buffer or for 10% of the file at max + # a full buffer is typically 30% of the configured cache value + sensibleBufferPerc = min(bufferBytes / size * 100.0 / 2.8, 10) + + # can wait for buffer? + # we're relying on integer based percentages coming from kodi's internal ProgressCache. + # with a typical device buffer of 20-160 MB, this might be less than 1% of available buffer based on the playing + # media item. If we're below that value, wait for a defined amount of time instead of being smart. + if sensibleBufferPerc >= 1.0: + if currentBufferPerc < sensibleBufferPerc: + # pause + wasPlaying = False + if self.player.playState == self.player.STATE_PLAYING: + util.DEBUG_LOG("SeekDialog.buffer: Waiting for buffer to reach {} (is: {}), pausing" + .format(sensibleBufferPerc, currentBufferPerc)) + self.player.pause() + wasPlaying = True + + waitedFor = 0 + waitMax = util.advancedSettings.bufferWaitMax + waitExceeded = False + self.waitingForBuffer = True + self.showOSD(focusButton=False) + with busy.BusyClosableMsgContext() as bc: + # check for the buffer fill-state every 200ms + # this may be canceled by the usual actions; + # depending on who receives the cancel action, _abortBufferWait might be set by our onAction + # or by the busy window via the context manager + while not self._abortBufferWait and not bc.shouldClose and waitedFor < waitMax and \ + (int(xbmc.getInfoLabel("Player.ProgressCache")) - + int(xbmc.getInfoLabel("Player.Progress"))) < sensibleBufferPerc: + curBuf = int(xbmc.getInfoLabel("Player.ProgressCache")) - \ + int(xbmc.getInfoLabel("Player.Progress")) + + bc.setMessage("Buffer: {} %".format(int(curBuf / sensibleBufferPerc * 100))) + + xbmc.sleep(200) + waitedFor += 0.2 + + # report buffer state every 10 seconds + if int(waitedFor) > 0 and int(waitedFor) % 10 == 0: + util.DEBUG_LOG("SeekDialog.buffer: " + "Buffer filled {}/{}".format(curBuf, sensibleBufferPerc)) + + # buffer wait canceled via busy window + if bc.shouldClose: + self._abortBufferWait = True + + # buffer timed out + if waitedFor >= waitMax: + waitExceeded = True + + self.waitingForBuffer = False + + if waitExceeded or self._abortBufferWait: + if not self._abortBufferWait: + util.showNotification(util.T(32917, "Couldn't fill buffer in time ({}s)").format(waitMax), + header="Buffer") + self.stop() + return True + + if self.player.playState == self.player.STATE_PAUSED and wasPlaying: + # resume + util.DEBUG_LOG("SeekDialog.buffer: Buffer filled, resuming") + self.player.pause() + return True + else: + util.DEBUG_LOG("SeekDialog.buffer: Buffer already filled, not waiting for buffer") + + else: + wait = util.advancedSettings.bufferInsufficientWait + util.DEBUG_LOG("SeekDialog.buffer: Buffer is too small for us to see, waiting {} seconds".format(wait)) + self.waitingForBuffer = True + + wasPlaying = False + if self.player.playState == self.player.STATE_PLAYING: + self.player.pause() + wasPlaying = True + + with busy.BusyClosableMsgContext() as bc: + bc.setMessage("Buffering") + util.MONITOR.waitForAbort(wait) + self.waitingForBuffer = False + if self.player.playState == self.player.STATE_PAUSED and wasPlaying: + self.player.pause() + return True + + def onPlayBackResumed(self): + util.DEBUG_LOG("SeekDialog: OnPlaybackResumed") + if self._ignoreInput: + self._ignoreInput = False + + self.idleTime = None + self.ldTimer and self.syncTimeKeeper() + + def onAVChange(self): + util.DEBUG_LOG("SeekDialog: OnAVChange: DPO: {0}, offset: {1}".format(self.DPPlayerOffset, self.offset)) + + # wait for buffer if we're not expecting a seek + if not self.handler.seekOnStart and util.getSetting("slow_connection", False) and not self.waitingForBuffer: + # fixme: not sure why this is necessary, but something breaks when playing back a next item from playback + # that doesn't have a seek value. Adding a slight delay here fixes that. Timing issue? + xbmc.sleep(100) + self.tick(waitForBuffer=True) + return + + def onAVStarted(self): + util.DEBUG_LOG("SeekDialog: OnAVStarted: DPO: {0}, offset: {1}".format(self.DPPlayerOffset, self.offset)) + if self._ignoreInput: + self._ignoreInput = False + + self.ldTimer and self.syncTimeKeeper() + + def onPlayBackStarted(self): + util.DEBUG_LOG("SeekDialog: OnPlaybackStarted") + if self._ignoreInput: + self._ignoreInput = False + + self.ldTimer and self.syncTimeKeeper() + + def onPlayBackPaused(self): + util.DEBUG_LOG("SeekDialog: OnPlaybackPaused") + self.idleTime = time.time() + + def onPlayBackSeek(self, stime, offset): + util.DEBUG_LOG("SeekDialog: OnPlaybackSeek: {0}, {1}".format(stime, offset)) + self.idleTime = None + self.ldTimer and self.syncTimeKeeper() + + def onPlayBackStopped(self): + util.DEBUG_LOG("SeekDialog: OnPlayBackStopped") + self.killTimeKeeper() + + def onPlayBackEnded(self): + util.DEBUG_LOG("SeekDialog: OnPlayBackEnded") + self.killTimeKeeper() + + def onPlayBackFailed(self): + util.DEBUG_LOG("SeekDialog: OnPlayBackFailed") + self.killTimeKeeper() + + def syncTimeKeeper(self): + if not not self.handler.player.playerObject: + return + + self.timeKeeperTime = self.trueOffset()#int(self.handler.player.getTime() * 1000) + if not self.timeKeeper: + self.timeKeeper = plexapp.util.RepeatingCounterTimer(1.0, self.onTimeKeeperCallback) + self.onTimeKeeperCallback(tick=False) + self.timeKeeper.reset() + + def killTimeKeeper(self): + if self.timeKeeper: + try: + self.timeKeeper.cancel() + self.timeKeeper.join() + self.timeKeeper = None + except: + util.ERROR("Couldn't stop timeKeeper") + + def onTimeKeeperCallback(self, tick=True): + """ + called by playbackTimer periodically, sets playback time/ends in UI + """ + # we might be a little early on slower systems + if not self.started or not self.handler.player.playerObject: + return + + if self.stopPlaybackOnIdle: + if self.idleTime and time.time() - self.idleTime >= self.stopPlaybackOnIdle: + util.LOG("Player has been idle for {}s, stopping.".format(int(time.time() - self.idleTime))) + self.handler.player.stopAndWait() + return + + if not self.idleTime and xbmc.getCondVisibility('Player.Paused'): + self.idleTime = time.time() + + if tick and xbmc.getCondVisibility('Player.HasVideo + Player.Playing'): + self.timeKeeperTime += 1000 + + # Update buffer state in PPI if open and old Kodi version + if util.KODI_BUILD_NUMBER < 2090821 and self.getProperty('show.PPI'): + try: + cache = int(xbmc.getInfoLabel('Player.ProgressCache')) - int(xbmc.getInfoLabel('Player.Progress')) + self.setProperty('ppi.Buffered', str(cache)) + except: + pass + + # Only updateCurrent when not in DirectPlay mode. Otherwise the Kodi time functions will be used by the skin. + if self.isDirectPlay: + return + + self.updateCurrent( + update_position_control=not self._seeking and not self._applyingSeek, atOffset=self.timeKeeperTime) + + @property + def countingDownMarker(self): + return self._currentMarker and \ + self._currentMarker["countdown"] is not None and \ + self._currentMarker["countdown"] > 0 + + def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=False, setSkipped=False, + offset=None): + # intro/credits marker display logic + markerDef = self.getCurrentMarkerDef(offset=offset) + + if not markerDef: + # no marker to display, hide it + self.setProperty('show.markerSkip', '') + self.setProperty('show.markerSkip_OSDOnly', '') + + # this might be counter intuitive, but self._currentMarker is a reference to a dict + if self._currentMarker: + self._currentMarker["countdown"] = None + self._currentMarker = None + return False + + # getCurrentMarkerDef might have overridden the startTimeOffset, use that + startTimeOff = markerDef["overrideStartOff"] if markerDef["overrideStartOff"] is not None else \ + int(markerDef["marker"].startTimeOffset) + + markerAutoSkip = getattr(self, markerDef["markerAutoSkip"]) + markerAutoSkipped = markerDef["markerAutoSkipped"] + + sTOffWThres = startTimeOff + util.advancedSettings.autoSkipOffset * 1000 + + # we just want to return an early marker if we want to autoSkip it, so we can tell the handler to seekOnStart + if onlyReturnIntroMD and markerDef["marker_type"] == "intro" and markerAutoSkip: + if startTimeOff == 0 and not markerDef["markerAutoSkipped"]: + if setSkipped: + markerDef["markerAutoSkipped"] = True + return int(markerDef["marker"].endTimeOffset) + 1000 + return False + + if cancelTimer and self.countingDownMarker: + self._currentMarker["countdown"] = None + markerDef["markerAutoSkipped"] = True + setattr(self, markerDef["markerAutoSkipShownTimer"], None) + self.setProperty('show.markerSkip', '') + return False + + autoSkippingNow = markerDef \ + and markerAutoSkip \ + and not markerAutoSkipped \ + and not self._navigatedViaMarkerOrChapter \ + and (markerDef["countdown"] == 0 or startTimeOff == 0 or immediate) + # and (startTimeOff == 0 or sTOffWThres <= self.offset) \ + + # auto skip marker + # delay marker autoskip by autoSkipOffset to avoid cutting off content at the expense of being + # slightly too late + if autoSkippingNow: + markerDef["markerAutoSkipped"] = True + setattr(self, markerDef["markerAutoSkipShownTimer"], None) + self.setProperty('show.markerSkip', '') + self.setProperty('show.markerSkip_OSDOnly', '') + self.resetAutoSeekTimer(None) + final = getattr(markerDef["marker"], "final", False) + markerDef["countdown"] = None + + if final: + # final marker is _not_ at the end of video, seek and do nothing + if int(markerDef["marker"].endTimeOffset) < self.duration - FINAL_MARKER_NEGOFF: + target = int(markerDef["marker"].endTimeOffset) + util.DEBUG_LOG( + "MarkerAutoSkip: Skipping final marker, its endTime is too early, " + "though, seeking and playing back") + self.doSeek(target) + return False + + # tell plex we've arrived at the end of the video, playing back + self.handler.updateNowPlaying(True, state=self.player.STATE_STOPPED, time=self.duration - 1000) + + # go to next video immediately if on bingeMode + if self.handler.playlist and self.handler.playlist.hasNext() and self.bingeMode: + if not self.handler.queuingNext: + # skip final marker + util.DEBUG_LOG("MarkerAutoSkip: Skipping final marker, going to next video") + self.handler.ignoreTimelines = True + self.handler.queuingNext = True + self._ignoreTick = True + self._ignoreInput = True + self.killTimeKeeper() + self.player.stop() + return True + else: + util.DEBUG_LOG("MarkerAutoSkip: Skipping final marker, stopping") + self.stop() + return False + + util.DEBUG_LOG('MarkerAutoSkip: Skipping marker {}'.format(markerDef["marker"])) + self.doSeek(int(markerDef["marker"].endTimeOffset) + 1000) + return True + + # got a marker, display logic + # hide marker into OSD after a timeout + timer = getattr(self, markerDef["markerAutoSkipShownTimer"]) + + if timer is None or self.player.playState == self.player.STATE_PAUSED: + setattr(self, markerDef["markerAutoSkipShownTimer"], time.time()) + + else: + if timer + getattr(self, markerDef["markerSkipBtnTimeout"]) <= time.time(): + self.setProperty('show.markerSkip_OSDOnly', '1') + else: + self.setProperty('show.markerSkip_OSDOnly', '') + + # no marker auto skip or not yet auto skipped, normal display + if not markerAutoSkip or (markerAutoSkip and not markerAutoSkipped): + self.setProperty('show.markerSkip', '1') + # marker auto skip and already skipped - hide in OSD + elif markerAutoSkip and markerAutoSkipped: + self.setProperty('show.markerSkip_OSDOnly', '1') + + # set marker name, count down + if markerAutoSkip and not markerAutoSkipped: + if markerDef["countdown"] is None: + # reset countdown on new marker + if not self._currentMarker or self._currentMarker != markerDef or markerDef["countdown"] is None: + # fixme: round might not be right here, but who cares + markerDef["countdown"] = int(max(round((sTOffWThres - self.trueOffset()) / 1000.0) + 1, 1)) + + if self.player.playState == self.player.STATE_PLAYING and not self.osdVisible(): + markerDef["countdown"] -= 1 + + self.setProperty('marker.countdown', '1') + + if markerDef["countdown"] > 0: + markerName = "{} ({})".format(markerDef["autoSkipName"], markerDef["countdown"]) + else: + markerName = markerDef["autoSkipName"] + else: + markerName = markerDef["name"] + self.setProperty('skipMarkerName', markerName) + + # store current marker + self._currentMarker = markerDef + + # focus marker if OSD is hidden, last focus wasn't the marker button and we're not auto skipping this marker + if not self.osdVisible() and self.lastFocusID != self.SKIP_MARKER_BUTTON_ID and \ + not self.getProperty('show.markerSkip_OSDOnly') and self.getProperty('show.markerSkip') \ + and not markerAutoSkip: + self.setFocusId(self.SKIP_MARKER_BUTTON_ID) + + def tick(self, offset=None, waitForBuffer=False): + """ + Called ~1/s; can be wildly inaccurate. + """ + if waitForBuffer: + cont = self.waitForBuffer() + if not cont: + return + + if not self.initialized or self._ignoreTick: + return + + # invisibly sync low-drift timer to current playback every X seconds, as Player.getTime() can be wildly off + if self.ldTimer and not self.osdVisible() and self.timeKeeper and self.timeKeeper.ticks >= 60: + self.syncTimeKeeper() + + cancelTick = False + # don't auto skip while we're initializing and waiting for the handler to seek on start + if offset is None and not self.handler.seekOnStart: + cancelTick = self.displayMarkers() + + if cancelTick: + return + + if xbmc.getCondVisibility('Window.IsActive(busydialog) + !Player.Caching'): + util.DEBUG_LOG('SeekDialog: Possible stuck busy dialog - closing') + xbmc.executebuiltin('Dialog.Close(busydialog,1)') + + if not self.hasDialog and not self.playlistDialogVisible and self.osdVisible(): + if time.time() > self.timeout and not self._osdHideFast: + if not xbmc.getCondVisibility('Window.IsActive(videoosd) | Player.Rewinding | Player.Forwarding'): + self.hideOSD() + + # try insta-hiding the OSDs when playback was requested + elif self._osdHideFast: + xbmc.executebuiltin('Dialog.Close(videoosd,true)') + xbmc.executebuiltin('Dialog.Close(seekbar,true)') + if not xbmc.getCondVisibility('Window.IsActive(videoosd) | Player.Rewinding | Player.Forwarding'): + self.hideOSD() + + self._osdHideFast = False + + try: + self.offset = offset or int(self.handler.player.getTime() * 1000) + except RuntimeError: # Playback has stopped + self.resetSeeking() + return + + if offset or (self.autoSeekTimeout and time.time() >= self.autoSeekTimeout and + self.offset != self.selectedOffset): + self.resetAutoSeekTimer(None) + #off = offset is not None and offset or None + #self.doSeek(off) + self.doSeek() + return True + + if self.isDirectPlay or not self.ldTimer: + self.updateCurrent(update_position_control=not self._seeking and not self._applyingSeek) + + @property + def playlistDialogVisible(self): + return self._playlistDialogVisible + + @playlistDialogVisible.setter + def playlistDialogVisible(self, value): + self._playlistDialogVisible = value + self.setProperty('playlist.visible', '1' if value else '') + + def showPlaylistDialog(self): + if not self.playlistDialog: + self.playlistDialog = PlaylistDialog.create(show=False, handler=self.handler) + + self.playlistDialogVisible = True + self.playlistDialog.doModal() + self.resetTimeout() + self.playlistDialogVisible = False + + def osdVisible(self): + return xbmc.getCondVisibility('Control.IsVisible(801)') + + def showOSD(self, focusButton=True): + self.setProperty('show.OSD', '1') + xbmc.executebuiltin('Dialog.Close(videoosd,true)') + if xbmc.getCondVisibility('Player.showinfo'): + xbmc.executebuiltin('Action(Info)') + + if focusButton: + self.setFocusId(self.PLAY_PAUSE_BUTTON_ID) + + def hideOSD(self, skipMarkerFocus=False, closing=False): + self.setProperty('show.OSD', '') + if closing: + return + + self.setFocusId(self.NO_OSD_BUTTON_ID) + if not skipMarkerFocus and self.getCurrentMarkerDef() and not self.getProperty('show.markerSkip_OSDOnly'): + self.setFocusId(self.SKIP_MARKER_BUTTON_ID) + + self.resetSeeking() + self._osdHideAnimationTimeout = time.time() + self.OSD_HIDE_ANIMATION_DURATION + + self._osdHideFast = False + if self.playlistDialog: + self.playlistDialog.doClose() + self.playlistDialogVisible = False + + +class PlaylistDialog(kodigui.BaseDialog): + xmlFile = 'script-plex-video_current_playlist.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + LI_AR16X9_THUMB_DIM = (178, 100) + LI_SQUARE_THUMB_DIM = (100, 100) + + PLAYLIST_LIST_ID = 101 + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + self.handler = kwargs.get('handler') + self.playlist = self.handler.playlist + + def onFirstInit(self): + self.handler.player.on('playlist.changed', self.playQueueCallback) + self.handler.player.on('session.ended', self.sessionEnded) + self.playlistListControl = kodigui.ManagedControlList(self, self.PLAYLIST_LIST_ID, 6) + self.fillPlaylist() + self.updatePlayingItem() + self.setFocusId(self.PLAYLIST_LIST_ID) + + def onClick(self, controlID): + if controlID == self.PLAYLIST_LIST_ID: + self.playlistListClicked() + + def playlistListClicked(self): + mli = self.playlistListControl.getSelectedItem() + if not mli: + return + self.handler.playAt(mli.pos()) + self.updatePlayingItem() + + def sessionEnded(self, **kwargs): + util.DEBUG_LOG('Video OSD: Session ended - closing') + self.doClose() + + def createListItem(self, pi): + if pi.type == 'episode': + return self.createEpisodeListItem(pi) + elif pi.type in ('movie', 'clip'): + return self.createMovieListItem(pi) + + def createEpisodeListItem(self, episode): + label2 = u'{0} \u2022 {1}'.format( + episode.grandparentTitle, + u'{0}{1} \u2022 {2}{3}'.format(T(32310, 'S'), episode.parentIndex, T(32311, 'E'), episode.index) + ) + mli = kodigui.ManagedListItem(episode.title, label2, + thumbnailImage=episode.thumb.asTranscodedImageURL(*self.LI_AR16X9_THUMB_DIM), + data_source=episode) + mli.setProperty('track.duration', util.durationToShortText(episode.duration.asInt())) + mli.setProperty('video', '1') + mli.setProperty('watched', episode.isWatched and '1' or '') + return mli + + def createMovieListItem(self, movie): + mli = kodigui.ManagedListItem(movie.title, movie.year, + thumbnailImage=movie.art.asTranscodedImageURL(*self.LI_AR16X9_THUMB_DIM), + data_source=movie) + mli.setProperty('track.duration', util.durationToShortText(movie.duration.asInt())) + mli.setProperty('video', '1') + mli.setProperty('watched', movie.isWatched and '1' or '') + return mli + + def playQueueCallback(self, **kwargs): + mli = self.playlistListControl.getSelectedItem() + pi = mli.dataSource + plexID = pi['comment'].split(':', 1)[0] + viewPos = self.playlistListControl.getViewPosition() + + self.fillPlaylist() + + for ni in self.playlistListControl: + if ni.dataSource['comment'].split(':', 1)[0] == plexID: + self.playlistListControl.selectItem(ni.pos()) + break + + xbmc.sleep(100) + + newViewPos = self.playlistListControl.getViewPosition() + if viewPos != newViewPos: + diff = newViewPos - viewPos + self.playlistListControl.shiftView(diff, True) + + def updatePlayingItem(self): + playing = self.handler.player.video.ratingKey + selectIndex = None + for (index, mli) in enumerate(self.playlistListControl): + isMLI = mli.dataSource.ratingKey == playing + mli.setProperty('playing', isMLI and '1' or '') + if isMLI: + selectIndex = index + + if selectIndex is not None: + xbmc.executebuiltin('Control.SetFocus({0}, {1})'.format(self.PLAYLIST_LIST_ID, selectIndex)) + + def fillPlaylist(self): + items = [] + idx = 1 + for pi in self.playlist.items(): + mli = self.createListItem(pi) + if mli: + mli.setProperty('track.number', str(idx)) + items.append(mli) + idx += 1 + + self.playlistListControl.reset() + self.playlistListControl.addItems(items) diff --git a/script.plexmod/lib/windows/settings.py b/script.plexmod/lib/windows/settings.py new file mode 100644 index 000000000..66e1c220e --- /dev/null +++ b/script.plexmod/lib/windows/settings.py @@ -0,0 +1,836 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from kodi_six import xbmcvfs +from . import kodigui +from . import windowutils + +from lib import util +from lib.util import T + +import plexnet +import sys + + +class Setting(object): + type = None + ID = None + label = None + desc = None + default = None + userAware = False + + def translate(self, val): + return str(val) + + def get(self): + return util.getSetting(self.ID, self.default) + + def set(self, val): + old = self.get() + setRet = util.setSetting(self.ID, val) + if old != val: + util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]'.format(self.ID, old, val)) + plexnet.util.APP.trigger('change:{0}'.format(self.ID), value=val) + return setRet + + def valueLabel(self): + return self.translate(self.get()) + + def __repr__(self): + return ''.format(self.ID, self.get()) + + +class BasicSetting(Setting): + def __init__(self, ID, label, default, desc=''): + self.ID = ID + self.label = label + self.default = default + self.desc = desc + + def description(self, desc): + self.desc = desc + return self + + +class ListSetting(BasicSetting): + type = 'LIST' + options = () + + def translate(self, val): + return self.options[len(self.options) - 1 - val] + + def optionLabels(self): + return self.options + + def optionIndex(self): + return len(self.options) - 1 - self.get() + + def set(self, val): + BasicSetting.set(self, len(self.options) - 1 - val) + + +class QualitySetting(ListSetting): + options = ( + T(32001), + T(32002), + T(32003), + T(32004), + T(32005), + T(32006), + T(32007), + T(32008), + T(32009), + T(32010), + T(32011), + T(32012), + T(32013), + T(32014), + ) + + +class ThemeMusicSetting(ListSetting): + options = [ + T(32481), + ] + [T(32482) % {"percentage": 10+i} for i in range(0, 100, 10)] + + +class PlayedThresholdSetting(ListSetting): + options = ['{} %'.format(perc) for perc in range(70, 100, 5)] + + +class BoolSetting(BasicSetting): + type = 'BOOL' + + +class BoolUserSetting(BoolSetting): + """ + A user-aware BoolSetting + """ + userAware = True + + @property + def userAwareID(self): + return '{}.{}'.format(self.ID, plexnet.plexapp.ACCOUNT.ID) + + def get(self): + return util.getSetting(self.userAwareID, self.default) + + def set(self, val): + old = self.get() + if old != val: + util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]'.format(self.userAwareID, old, val)) + plexnet.util.APP.trigger('change:{0}'.format(self.ID), key=self.userAwareID, value=val, skey=self.ID) + return util.setSetting(self.userAwareID, val) + + +class OptionsSetting(BasicSetting): + type = 'OPTIONS' + + def __init__(self, ID, label, default, options): + BasicSetting.__init__(self, ID, label, default) + self.options = options + + def translate(self, val): + for ID, label in self.options: + if ID == val: + return label + + def optionLabels(self): + return [o[1] for o in self.options] + + def optionIndex(self): + val = self.get() + for i, o in enumerate(self.options): + if val == o[0]: + return i + + return 0 + + +class BufferSetting(OptionsSetting): + def get(self): + return util.kcm.memorySize + + def set(self, val): + old = self.get() + if old != val: + util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]'.format(self.ID, old, val)) + plexnet.util.APP.trigger('change:{0}'.format(self.ID), value=val) + + util.kcm.write(memorySize=val) + + +class ReadFactorSetting(OptionsSetting): + def get(self): + return util.kcm.readFactor + + def set(self, val): + old = self.get() + if old != val: + util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]'.format(self.ID, old, val)) + plexnet.util.APP.trigger('change:{0}'.format(self.ID), value=val) + + util.kcm.write(readFactor=val) + + +class InfoSetting(BasicSetting): + type = 'INFO' + + def __init__(self, ID, label, info): + BasicSetting.__init__(self, ID, label, None) + self.info = info + + def valueLabel(self): + return self.info + + +class PlatformSetting(InfoSetting): + def __init__(self): + InfoSetting.__init__(self, None, None, None) + self.ID = 'platfom_version' + self.label = T(32410, 'Platform Version') + + def valueLabel(self): + plat = None + try: + if sys.version_info[0] >= 3: + from lib import distro + dist = distro.linux_distribution() + if dist and len(dist) > 1: + plat = u'{0} {1}'.format(dist[0], dist[1]) + else: + import platform + dist = platform. dist() + if dist and len(dist) > 1: + plat = u'{0} {1}'.format(dist[0], dist[1]) + else: + plat = platform.platform() + plat = u'{0} {1}'.format(plat[0], '.'.join(plat[1].split('.', 2)[:2])) + except: + util.ERROR() + + plat = plat.strip() + + if not plat: + if xbmc.getCondVisibility('System.Platform.Android'): + plat = 'Android' + elif xbmc.getCondVisibility('System.Platform.OSX'): + plat = 'OSX' + elif xbmc.getCondVisibility('System.Platform.Darwin'): + plat = 'Darwin' + elif xbmc.getCondVisibility('System.Platform.Linux.RaspberryPi'): + plat = 'Linux (RPi)' + elif xbmc.getCondVisibility('System.Platform.Linux'): + plat = 'Linux' + elif xbmc.getCondVisibility('System.Platform.Windows'): + plat = 'Windows' + + return plat or T(32411, 'Unknown') + + +class ServerVersionSetting(InfoSetting): + def valueLabel(self): + if not plexnet.plexapp.SERVERMANAGER.selectedServer: + return '' + + return plexnet.plexapp.SERVERMANAGER.selectedServer.rawVersion or '' + + +class IPSetting(BasicSetting): + type = 'IP' + + +class IntegerSetting(BasicSetting): + type = 'INTEGER' + + +class Settings(object): + SETTINGS = { + 'main': ( + T(32000, 'Main'), ( + BoolSetting( + 'auto_signin', T(32038, 'Automatically Sign In'), False + ).description( + T(32100, 'Skip user selection and pin entry on startup.') + ), + BoolSetting( + 'speedy_home_hubs2', T(33503, 'Use alternative hubs refresh'), False + ).description( + T( + 33504, + "Refreshes all hubs for all libraries after an item's watch-state has changed, instead of " + "only those likely affected. Use this if you find a hub that doesn't update properly." + ) + ), + BoolSetting( + 'search_use_kodi_kbd', T(32955, 'Use Kodi keyboard for searching'), False + ), + ThemeMusicSetting('theme_music', T(32480, 'Theme music'), 5), + PlayedThresholdSetting('played_threshold', T(33501, 'Video played threshold'), 1).description( + T( + 33502, + "Set this to the same value as your Plex server (Settings>Library>Video played threshold) to av" + "oid certain pitfalls, Default: 90 %" + ) + ) + ) + ), + 'video': ( + T(32053, 'Video'), ( + QualitySetting('local_quality', T(32020, 'Local Quality'), 13), + QualitySetting('remote_quality', T(32021, 'Remote Quality'), 13), + QualitySetting('online_quality', T(32022, 'Online Quality'), 13), + BoolSetting('playback_directplay', T(32025, 'Allow Direct Play'), True), + BoolSetting('playback_remux', T(32026, 'Allow Direct Stream'), True), + BoolSetting('allow_4k', T(32036, 'Allow 4K'), True).description( + T(32102, 'Enable this if your hardware can handle 4K playback. Disable it to force transcoding.') + ), + BoolSetting('allow_hevc', T(32037, 'Allow HEVC (h265)'), True).description( + T(32103, 'Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding.') + ) + ) + ), + 'audio': ( + T(32931, 'Audio/Subtitles'), ( + BoolSetting('audio_clamp_to_orig', T(32058, 'Never exceed original audio codec'), True).description( + T(32059, 'When transcoding audio, never exceed the original audio bitrate or channel ' + 'count on the same codec.') + ), + BoolSetting('audio_channels_kodi', T(32060, 'Use Kodi audio channels'), + False).description( + T(32061, 'When transcoding audio, target the audio channels set in Kodi.') + ), + OptionsSetting( + 'audio_force_ac3_cond', + T(32062, 'Transcode audio to AC3'), + 'never', + ( + ('never', T(32033, 'Never')), + ('always', T(32028, 'Always')), + ('2', '2.1+'), + ('5', '5.1+'), + ) + ).description( + T(32063, 'Transcode audio to AC3 in certain conditions (useful for passthrough).') + ), + BoolSetting('audio_ac3dts', T(32064, 'Treat DTS like AC3'), + True).description( + T(32065, 'When force AC3 settings are enabled, treat DTS the same as AC3 ' + '(useful for Optical passthrough)') + ), + OptionsSetting( + 'burn_subtitles', + T(32031, 'Burn-in Subtitles'), + 'auto', + (('auto', T(32030, 'Auto')), ('image', T(32029, 'Only Image Formats')), + ('always', T(32028, 'Always'))) + ), + BoolSetting('burn_ssa', T(32944, 'Burn-in SSA subtitles'), + True).description( + T(32945, 'When Direct Streaming instruct the Plex Server to burn in SSA/ASS subtitles (thus ' + 'transcoding the video stream). If disabled it will not touch the video stream, but ' + 'will convert the subtitle to unstyled text.') + ), + BoolSetting('forced_subtitles_override', T(32941, 'Forced subtitles fix'), + False).description( + T(32493, 'When a media file has a forced/foreign subtitle for a subtitle-enabled language, the Plex' + ' Media Server preselects it. This behaviour is usually not necessary and not configurable' + '. This setting fixes that by ignoring the PMSs decision and selecting the same language ' + 'without a forced flag if possible.') + ), + BoolSetting('calculate_oshash', T(32958, 'Calculate OpenSubtitles.com hash'), + False).description( + T(32959, '') + ), + ) + ), + 'player': ( + T(32940, 'Player UI'), ( + BoolSetting('subtitle_downloads', T(32932, 'Show subtitle quick-actions button'), False).description( + T(32939, 'Only applies to video player UI')), + BoolSetting('video_show_ffwdrwd', T(32933, 'Show FFWD/RWD buttons'), False).description( + T(32939, 'Only applies to video player UI')), + BoolSetting('video_show_repeat', T(32934, 'Show repeat button'), False).description( + T(32939, 'Only applies to video player UI')), + BoolSetting('video_show_shuffle', T(32935, 'Show shuffle button'), False).description( + T(32939, 'Only applies to video player UI')), + OptionsSetting( + 'video_show_playlist', T(32936, 'Show playlist button'), 'eponly', + ( + ('always', T(32035, 'Always')), ('eponly', T(32938, 'Only for Episodes')), + ('never', T(32033, 'Never')) + ) + ).description(T(32939, 'Only applies to video player UI')), + OptionsSetting( + 'video_show_prevnext', T(32937, 'Show prev/next button'), 'eponly', + ( + ('always', T(32035, 'Always')), ('eponly', T(32938, 'Only for Episodes')), + ('never', T(32033, 'Never')) + ) + ).description(T(32939, 'Only applies to video player UI')), + ) + ), + 'player_user': ( + T(32631, 'Playback (user-specific)'), ( + BoolUserSetting( + 'post_play_auto', T(32039, 'Post Play Auto Play'), True + ).description( + T( + 32101, + "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatical" + "ly be played after a 15 second delay." + ) + ), + BoolUserSetting( + 'binge_mode', T(33618, 'TV binge-viewing mode'), False + ).description( + T(33619, 'Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t ' + 'skip the intro of the first episode of a season.\n\nCan be disabled/enabled per TV show.') + ), + BoolUserSetting( + 'auto_skip_intro', T(32522, 'Automatically Skip Intro'), False + ).description( + T(32523, 'Automatically skip intros if available. Doesn\'t override enabled binge mode.') + ), + BoolUserSetting( + 'auto_skip_credits', T(32526, 'Auto Skip Credits'), False + ).description( + T(32527, 'Automatically skip credits if available. Doesn\'t override enabled binge mode.') + ), + BoolUserSetting( + 'show_intro_skip_early', T(33505, 'Show intro skip button early'), False + ).description( + T(33506, 'Show the intro skip button from the start of a video with an intro marker. The auto-skipp' + 'ing setting applies. Doesn\'t override enabled binge mode.') + ), + BoolUserSetting( + 'auto_skip_in_transcode', T(32948, 'Allow auto-skip when transcoding'), True + ).description( + T(32949, 'When transcoding/DirectStreaming, allow auto-skip functionality.') + ), + BoolUserSetting( + 'show_chapters', T(33601, 'Show video chapters'), True + ).description( + T(33602, 'If available, show video chapters from the video-file instead of the ' + 'timeline-big-seek-steps.') + ), + BoolUserSetting( + 'virtual_chapters', T(33603, 'Use virtual chapters'), True + ).description( + T(33604, 'When the above is enabled and no video chapters are available, simulate them by using the' + ' markers identified by the Plex Server (Intro, Credits).') + ), + ) + ), + 'network': ( + T(33624, 'Network'), ( + OptionsSetting( + 'allow_insecure', T(32032), 'never', + (('never', T(32033)), ('same_network', T(32034)), ('always', T(32035))) + ).description( + T(32104, 'When to connect to servers with no secure connections...') + ), + BoolSetting('smart_discover_local', T(33625, 'Smart LAN/local server discovery'), True) + .description( + T(33626, "Checks whether servers returned from Plex.tv are actually local/in your LAN. " + "For specific setups (e.g. Docker) Plex.tv might not properly detect a local " + "server.\n\nNOTE: Only works on Kodi 19 or above." + ) + ), + BoolSetting('prefer_local', T(33627, 'Prefer LAN/local servers over security'), False) + .description( + T(33628, "Prioritizes local connections over secure ones. Needs the proper setting in \"Allow " + "Insecure Connections\" and the Plex Server's \"Secure connections\" at \"Preferred\". " + "Can be used to enforce manual servers." + ) + ), + BoolSetting('gdm_discovery', T(32042, 'Server Discovery (GDM)'), False), + IPSetting('manual_ip_0', T(32044, 'Connection 1 IP'), ''), + IntegerSetting('manual_port_0', T(32045, 'Connection 1 Port'), 32400), + IPSetting('manual_ip_1', T(32046, 'Connection 2 IP'), ''), + IntegerSetting('manual_port_1', T(32047, 'Connection 2 Port'), 32400), + ) + ), + 'system': ( + T(33600, 'System'), ( + + BoolSetting('kiosk.mode', T(32043, 'Start Plex On Kodi Startup'), False), + BufferSetting('cache_size', + T(33613, 'Kodi Buffer Size (MB)'), + 20, + [(mem, '{} MB'.format(mem)) for mem in util.kcm.viableOptions]) + .description( + '{}{}'.format(T(33614, 'stub1').format( + util.kcm.free, util.kcm.recMax), + '' if util.kcm.useModernAPI else ' '+T(32954, 'stub2')) + ), + ReadFactorSetting('readfactor', + T(32922, 'Kodi Cache Readfactor'), + 4, + [(rf, str(rf)) for rf in util.kcm.readFactorOpts]) + .description( + T(32923, 'Sets the Kodi cache readfactor value. Default: {0}, recommended: {1}.' + 'With "Slow connection" enabled this will be set to {2}, as otherwise the cache doesn\'t' + 'fill fast/aggressively enough.').format(util.kcm.defRF, + "{}-{}".format(*util.kcm.recRFRange), + util.kcm.defRFSM) + ), + BoolSetting( + 'slow_connection', T(32915, 'Slow connection'), False + ).description("Use with a wonky/slow connection, e.g. in a hotel room. Adjusts the UI to visually " + "wait for item refreshes and waits for the buffer to fill when starting playback."), + OptionsSetting( + 'action_on_sleep', + T(32700, 'Action on Sleep event'), + 'none', + (('none', T(32702, 'Nothing')), ('stop', T(32703, 'Stop playback')), + ('quit', T(32704, 'Quit Kodi')), ('reboot', T(32426, 'Reboot')), + ('shutdown', T(32423, 'Shutdown')), + ('hibernate', T(32425, 'Hibernate')), ('suspend', T(32424, 'Suspend')), + ('cecstandby', T(32705, 'CEC Standby')), ('logoff', T(32421, 'Sign Out'))) + ).description(T(32701, 'When Kodi receives a sleep event from the system, run the following action.')), + OptionsSetting( + 'player_stop_on_idle', + T(32946, 'Stop video playback on idle after'), + 0, + ((0, T(32033, 'Never')), (30, '30s'), (60, '1m'), (120, '2m'), (300, '5m'), (600, '10m'), + (900, '15m'), (1200, '20m'), (1800, '30m'), (2700, '45m'), (3600, '1h'),) + ), + BoolSetting( + 'player_stop_on_screensaver', T(32947, 'Stop video playback on screensaver'), True + ), + BoolSetting('debug', T(32024, 'Debug Logging'), False), + ) + ), + 'privacy': ( + T(32051, 'Privacy'), + () + ), + 'about': ( + T(32052, 'About'), ( + InfoSetting('addon_version', T(32054, 'Addon Version'), util.ADDON.getAddonInfo('version')), + InfoSetting('kodi_version', T(32055, 'Kodi Version'), xbmc.getInfoLabel('System.BuildVersion')), + PlatformSetting(), + InfoSetting('screen_res', T(32056, 'Screen Resolution'), + xbmc.getInfoLabel('System.ScreenResolution').split('-')[0].strip()), + ServerVersionSetting('server_version', T(32057, 'Current Server Version'), None), + InfoSetting('addon_path', T(33616, 'Addon Path'), util.ADDON.getAddonInfo("path")), + InfoSetting('userdata_path', T(33617, 'Userdata/Profile Path'), + util.translatePath("special://profile")), + ) + ), + } + + SECTION_IDS = ('main', 'video', 'audio', 'player', 'player_user', 'network', 'system', 'about') + + def __getitem__(self, key): + return self.SETTINGS[key] + + +# enable AV1 setting if kodi nexus +if util.KODI_VERSION_MAJOR >= 20: + videoSettings = list(Settings.SETTINGS["video"]) + videoSettings[1] = tuple(list(videoSettings[1]) + [ + BoolSetting('allow_av1', T(32601, 'Allow AV1'), False).description( + T(32602, + 'Enable this if your hardware can handle AV1. Disable it to force transcoding.') + ) + ]) + Settings.SETTINGS["video"] = (videoSettings[0], videoSettings[1]) + + +class SettingsWindow(kodigui.BaseWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-settings.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + SECTION_LIST_ID = 75 + SETTINGS_LIST_ID = 100 + OPTIONS_LIST_ID = 125 + TOP_GROUP_ID = 200 + + CLOSE_BUTTON_ID = 201 + PLAYER_STATUS_BUTTON_ID = 204 + + def onFirstInit(self): + self.settings = Settings() + self.sectionList = kodigui.ManagedControlList(self, self.SECTION_LIST_ID, 6) + self.settingsList = kodigui.ManagedControlList(self, self.SETTINGS_LIST_ID, 6) + self.optionsList = kodigui.ManagedControlList(self, self.OPTIONS_LIST_ID, 6) + + self.setProperty('heading', T(32343, 'Settings')) + self.showSections() + self.setFocusId(75) + self.lastSection = None + self.checkSection() + + def onAction(self, action): + try: + self.checkSection() + controlID = self.getFocusId() + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU): + if self.getFocusId() == self.OPTIONS_LIST_ID: + self.setFocusId(self.SETTINGS_LIST_ID) + return + # elif not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.TOP_GROUP_ID)): + # self.setFocusId(self.TOP_GROUP_ID) + # return + elif action == xbmcgui.ACTION_MOVE_RIGHT and controlID == 150: + self.editSetting(from_right=True) + except: + util.ERROR() + + kodigui.BaseWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.SECTION_LIST_ID: + self.setFocusId(self.SETTINGS_LIST_ID) + elif controlID == self.SETTINGS_LIST_ID: + self.editSetting() + elif controlID == self.OPTIONS_LIST_ID: + self.changeSetting() + elif controlID == self.CLOSE_BUTTON_ID: + self.doClose() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + + def checkSection(self): + mli = self.sectionList.getSelectedItem() + if not mli: + return + + if mli.dataSource == self.lastSection: + return + + self.lastSection = mli.dataSource + self.showSettings(self.lastSection) + self.setProperty('section.about', self.lastSection == 'about' and '1' or '') + util.DEBUG_LOG('Settings: Changed section ({0})'.format(self.lastSection)) + + def showSections(self): + items = [] + for sectionID in self.settings.SECTION_IDS: + label = self.settings[sectionID][0] + item = kodigui.ManagedListItem(label, data_source=sectionID) + items.append(item) + + self.sectionList.addItems(items) + + def showSettings(self, section): + settings = self.settings[section][1] + if not settings: + return self.settingsList.reset() + + items = [] + for setting in settings: + item = kodigui.ManagedListItem(setting.label, setting.type != 'BOOL' and setting.valueLabel() or '', data_source=setting) + item.setProperty('description', setting.desc) + if setting.type == 'BOOL': + item.setProperty('checkbox', '1') + item.setProperty('checkbox.checked', setting.get() and '1' or '') + elif setting.type == 'BUTTON': + item.setProperty('button', '1') + + if setting.userAware: + item.setProperty('useraware', '1') + + items.append(item) + + self.settingsList.reset() + self.settingsList.addItems(items) + + def editSetting(self, from_right=False): + mli = self.settingsList.getSelectedItem() + if not mli: + return + + setting = mli.dataSource + + if setting.type in ('LIST', 'OPTIONS'): + self.fillList(setting) + elif setting.type == 'BOOL' and not from_right: + self.toggleBool(mli, setting) + elif setting.type == 'IP' and not from_right: + self.editIP(mli, setting) + elif setting.type == 'INTEGER' and not from_right: + self.editInteger(mli, setting) + elif setting.type == 'BUTTON': + self.buttonDialog(mli, setting) + + def changeSetting(self): + optionItem = self.optionsList.getSelectedItem() + if not optionItem: + return + + mli = self.settingsList.getSelectedItem() + if not mli: + return + + setting = mli.dataSource + + if setting.type == 'LIST': + setting.set(optionItem.pos()) + mli.setLabel2(setting.valueLabel()) + elif setting.type == 'OPTIONS': + setting.set(optionItem.dataSource) + mli.setLabel2(setting.valueLabel()) + + self.setFocusId(self.SETTINGS_LIST_ID) + + def fillList(self, setting): + items = [] + if setting.type == 'LIST': + for label in setting.optionLabels(): + items.append(kodigui.ManagedListItem(label)) + elif setting.type == 'OPTIONS': + for ID, label in setting.options: + items.append(kodigui.ManagedListItem(label, data_source=ID)) + + self.optionsList.reset() + self.optionsList.addItems(items) + self.optionsList.selectItem(setting.optionIndex()) + self.setFocusId(self.OPTIONS_LIST_ID) + + def toggleBool(self, mli, setting): + setting.set(not setting.get()) + mli.setProperty('checkbox.checked', setting.get() and '1' or '') + + def editIP(self, mli, setting): + current = setting.get() + edit = True + if current: + edit = xbmcgui.Dialog().yesno( + T(32412, 'Edit Or Clear'), + T(32413, 'Edit IP address or clear the current setting?'), + nolabel=T(32414, 'Clear'), + yeslabel=T(32415, 'Edit') + ) + + if edit: + result = xbmcgui.Dialog().input(T(32416, 'Enter IP Address'), current, xbmcgui.INPUT_IPADDRESS) + if not result: + return + else: + result = '' + + setting.set(result) + mli.setLabel2(result) + + def editInteger(self, mli, setting): + result = xbmcgui.Dialog().input(T(32417, 'Enter Port Number'), str(setting.get()), xbmcgui.INPUT_NUMERIC) + if not result: + return + setting.set(int(result)) + mli.setLabel2(result) + + +class SelectDialog(kodigui.BaseDialog, util.CronReceiver): + xmlFile = 'script-plex-settings_select_dialog.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + OPTIONS_LIST_ID = 100 + + def __init__(self, *args, **kwargs): + kodigui.BaseDialog.__init__(self, *args, **kwargs) + self.heading = kwargs.get('heading') + self.options = kwargs.get('options') + self.choice = None + + def onFirstInit(self): + self.optionsList = kodigui.ManagedControlList(self, self.OPTIONS_LIST_ID, 8) + self.setProperty('heading', self.heading) + self.showOptions() + util.CRON.registerReceiver(self) + + def onAction(self, action): + try: + if not xbmc.getCondVisibility('Player.HasMedia'): + self.doClose() + return + except: + util.ERROR() + + kodigui.BaseDialog.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.OPTIONS_LIST_ID: + self.setChoice() + + def onClosed(self): + util.CRON.cancelReceiver(self) + + def tick(self): + if not xbmc.getCondVisibility('Player.HasMedia'): + self.doClose() + return + + def setChoice(self): + mli = self.optionsList.getSelectedItem() + if not mli: + return + + self.choice = self.options[self.optionsList.getSelectedPosition()][0] + self.doClose() + + def showOptions(self): + items = [] + for o in self.options: + item = kodigui.ManagedListItem(o[1], data_source=o[0]) + items.append(item) + + self.optionsList.reset() + self.optionsList.addItems(items) + + self.setFocusId(self.OPTIONS_LIST_ID) + + +def showOptionsDialog(heading, options): + w = SelectDialog.open(heading=heading, options=options) + choice = w.choice + del w + return choice + + +def showAudioDialog(video): + options = [(s, s.getTitle()) for s in video.audioStreams] + choice = showOptionsDialog(T(32048, 'Audio'), options) + if choice is None: + return + + video.selectStream(choice) + + +def showSubtitlesDialog(video): + options = [(s, s.getTitle()) for s in video.subtitleStreams] + options.insert(0, (plexnet.plexstream.NoneStream(), 'None')) + choice = showOptionsDialog(T(32396, 'Subtitles'), options) + if choice is None: + return + + video.selectStream(choice) + + +def showQualityDialog(video): + options = [(13 - i, T(l)) for (i, l) in enumerate((32001, 32002, 32003, 32004, 32005, 32006, 32007, 32008, 32009, + 32010, 32011))] + + choice = showOptionsDialog(T(32397, 'Quality'), options) + if choice is None: + return + + video.settings.setPrefOverride('local_quality', choice) + video.settings.setPrefOverride('remote_quality', choice) + video.settings.setPrefOverride('online_quality', choice) + + +def openWindow(): + w = SettingsWindow.open() + del w diff --git a/script.plexmod/lib/windows/signin.py b/script.plexmod/lib/windows/signin.py new file mode 100644 index 000000000..444933662 --- /dev/null +++ b/script.plexmod/lib/windows/signin.py @@ -0,0 +1,146 @@ +from __future__ import absolute_import +from kodi_six import xbmcgui +from . import kodigui +from lib import util + + +class Background(kodigui.BaseWindow): + xmlFile = 'script-plex-signin_background.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + +class SignInMessage(kodigui.BaseWindow): + xmlFile = 'script-plex-signin_blank.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + SCREEN_BUTTON_ID = 100 + + def __init__(self, *args, **kwargs): + self.message = kwargs.get('message') + kodigui.BaseWindow.__init__(self, *args, **kwargs) + + def onFirstInit(self): + self.setProperty('message', self.message) + + def onClick(self, controlID): + if controlID == self.SCREEN_BUTTON_ID: + self.doClose() + + +class SignInPlexPass(kodigui.BaseWindow): + xmlFile = 'script-plex-plex_pass.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + RETRY_BUTTON_ID = 100 + + def __init__(self, *args, **kwargs): + self.retry = False + kodigui.BaseWindow.__init__(self, *args, **kwargs) + + def onAction(self, action): + if action == xbmcgui.ACTION_SELECT_ITEM: + self.retry = True + self.doClose() + + def onClick(self, controlID): + if controlID == self.RETRY_BUTTON_ID: + self.retry = True + self.doClose() + + +class PreSignInWindow(kodigui.BaseWindow): + xmlFile = 'script-plex-pre_signin.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + SIGNIN_BUTTON_ID = 100 + + def __init__(self, *args, **kwargs): + self.doSignin = False + kodigui.BaseWindow.__init__(self, *args, **kwargs) + + def onFirstInit(self): + self.signinButton = self.getControl(self.SIGNIN_BUTTON_ID) + + def onAction(self, action): + if action == xbmcgui.ACTION_SELECT_ITEM: + self.doSignin = True + self.doClose() + + def onClick(self, controlID): + if controlID == self.SIGNIN_BUTTON_ID: + self.doSignin = True + self.doClose() + + +class PinLoginWindow(kodigui.BaseWindow): + xmlFile = 'script-plex-pin_login.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + def __init__(self, *args, **kwargs): + self.abort = False + kodigui.BaseWindow.__init__(self, *args, **kwargs) + + def setPin(self, pin): + self.setProperty('pin.image.0', 'script.plex/sign_in/digits/{0}.png'.format(pin[0].upper())) + self.setProperty('pin.image.1', 'script.plex/sign_in/digits/{0}.png'.format(pin[1].upper())) + self.setProperty('pin.image.2', 'script.plex/sign_in/digits/{0}.png'.format(pin[2].upper())) + self.setProperty('pin.image.3', 'script.plex/sign_in/digits/{0}.png'.format(pin[3].upper())) + + def setLinking(self): + self.setProperty('linking', '1') + self.setProperty('pin.image.0', '') + self.setProperty('pin.image.1', '') + self.setProperty('pin.image.2', '') + self.setProperty('pin.image.3', '') + + def onAction(self, action): + try: + if action == xbmcgui.ACTION_NAV_BACK or action == xbmcgui.ACTION_PREVIOUS_MENU: + self.abort = True + except: + util.ERROR() + + kodigui.BaseWindow.onAction(self, action) + + +class ExpiredWindow(kodigui.BaseWindow): + xmlFile = 'script-plex-refresh_code.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + REFRESH_BUTTON_ID = 100 + + def __init__(self, *args, **kwargs): + self.refresh = False + kodigui.BaseWindow.__init__(self, *args, **kwargs) + + def onFirstInit(self): + self.refreshButton = self.getControl(self.REFRESH_BUTTON_ID) + + def onClick(self, controlID): + if controlID == self.REFRESH_BUTTON_ID: + self.refresh = True + self.doClose() diff --git a/script.plexmod/lib/windows/slidehshow.py b/script.plexmod/lib/windows/slidehshow.py new file mode 100644 index 000000000..d25f8b670 --- /dev/null +++ b/script.plexmod/lib/windows/slidehshow.py @@ -0,0 +1,108 @@ +import time +import random + +from . import kodigui + +from lib import util +from plexnet import plexapp + +class Slideshow(kodigui.BaseWindow, util.CronReceiver): + xmlFile = 'script-plex-slideshow.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + TIME_BETWEEN_IMAGES = 15 + TIME_HIDE_TITLE_IN_QUIZ = 5 + TIME_DISPLAY_MOVE = 60 + + CONTROL_INFO_GROUP = 100 + + def __init__(self, *args, **kwargs): + kodigui.BaseWindow.__init__(self, *args, **kwargs) + self.timeBetweenImages = self.TIME_BETWEEN_IMAGES + self.timeBetweenDisplayMove = self.TIME_DISPLAY_MOVE + self.timeTitleIsHidden = self.TIME_HIDE_TITLE_IN_QUIZ + self.quizMode = util.advancedSettings.screensaverQuiz + self.initialized = False + + def onFirstInit(self): + self.setProperty('clock', '') + self.setProperty('title', '') + self.setProperty('thumb', '') + self.setProperty('align', '0') + + self.infoGroupControl = self.getControl(self.CONTROL_INFO_GROUP) + + util.CRON.registerReceiver(self) + self.timeFormat = util.timeFormat.replace(":%S", "") + self.lastTime = '' + self.displayPosition = 0 + self.changeTime = time.time() - 1 + self.displayMoveTime = time.time() + self.timeBetweenDisplayMove + self.revealTitleTime = None + + self.selectedServer = plexapp.SERVERMANAGER.selectedServer + self.index = -1 + self.images = [] + + self.initialized = True + + def tick(self): + if not self.initialized: + return + + currentTime = time.time() + timestr = time.strftime(self.timeFormat, time.localtime(currentTime)) + if not util.padHour and timestr[0] == "0" and timestr[1] != ":": + timestr = timestr[1:] + + if currentTime > self.changeTime: + nextIndex = self.index + 1 + + if nextIndex >= len(self.images): + if self.selectedServer != None: + self.images = self.selectedServer.library.randomArts(); + util.DEBUG_LOG('[SS] Fetched {0} items'.format(len(self.images))) + nextIndex = 0 + + if len(self.images) == 0: + title = 'No Images' + url = '' + else: + image = self.images[nextIndex] + title = image.get('title') + key = image.get('key') + url = self.selectedServer.getImageTranscodeURL(key, self.width, self.height) + if not self.quizMode: + self.setProperty('title', title) + else: + self.setProperty('title', '') + self.quizTitle = title + self.revealTitleTime = currentTime + self.timeTitleIsHidden + self.setProperty('thumb', url) + + self.index = nextIndex + self.changeTime = time.time() + self.timeBetweenImages + + if self.revealTitleTime != None and currentTime > self.revealTitleTime: + self.setProperty('title', self.quizTitle) + self.revealTitleTime = None + + if currentTime > self.displayMoveTime: + oldDisplayPosition = self.displayPosition + self.displayPosition = (oldDisplayPosition + random.randint(1, 3)) % 4 + + if (oldDisplayPosition&2) != (self.displayPosition&2): + self.setProperty('align', str((self.displayPosition&2)>>1)) + + if (oldDisplayPosition&1) != (self.displayPosition&1): + newY = self.height - self.infoGroupControl.getY() - self.infoGroupControl.getHeight() + self.infoGroupControl.setPosition(self.infoGroupControl.getX(), newY) + + self.displayMoveTime = currentTime + self.timeBetweenDisplayMove + + if timestr != self.lastTime: + self.setProperty('clock', timestr) \ No newline at end of file diff --git a/script.plexmod/lib/windows/subitems.py b/script.plexmod/lib/windows/subitems.py new file mode 100644 index 000000000..13204429b --- /dev/null +++ b/script.plexmod/lib/windows/subitems.py @@ -0,0 +1,647 @@ +from __future__ import absolute_import +import gc + +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from lib import colors +from lib import util +from lib import metadata +from lib import player + +from plexnet import playlist +from plexnet.util import INTERFACE + +from . import busy +from . import episodes +from . import tracks +from . import opener +from . import info +from . import musicplayer +from . import videoplayer +from . import dropdown +from . import windowutils +from . import search +from . import pagination +from . import playbacksettings + +from lib.util import T +from .mixins import SeasonsMixin + + +class RelatedPaginator(pagination.BaseRelatedPaginator): + def getData(self, offset, amount): + return self.parentWindow.mediaItem.getRelated(offset=offset, limit=amount) + + +class ShowWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMixin, playbacksettings.PlaybackSettingsMixin): + xmlFile = 'script-plex-seasons.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + EXTRA_DIM = util.scaleResolution(329, 185) + RELATED_DIM = util.scaleResolution(268, 397) + ROLES_DIM = util.scaleResolution(334, 334) + + SUB_ITEM_LIST_ID = 400 + + EXTRA_LIST_ID = 401 + RELATED_LIST_ID = 402 + ROLES_LIST_ID = 403 + + OPTIONS_GROUP_ID = 200 + + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + PLAYER_STATUS_BUTTON_ID = 204 + + PROGRESS_IMAGE_ID = 250 + + INFO_BUTTON_ID = 301 + PLAY_BUTTON_ID = 302 + SHUFFLE_BUTTON_ID = 303 + OPTIONS_BUTTON_ID = 304 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.mediaItem = kwargs.get('media_item') + self.parentList = kwargs.get('parent_list') + self.cameFrom = kwargs.get('came_from') + self.mediaItems = None + self.exitCommand = None + self.lastFocusID = None + self.lastNonOptionsFocusID = None + self.initialized = False + self.relatedPaginator = None + + def doClose(self): + self.relatedPaginator = None + kodigui.ControlledWindow.doClose(self) + + def onFirstInit(self): + self.subItemListControl = kodigui.ManagedControlList(self, self.SUB_ITEM_LIST_ID, 5) + self.extraListControl = kodigui.ManagedControlList(self, self.EXTRA_LIST_ID, 5) + self.relatedListControl = kodigui.ManagedControlList(self, self.RELATED_LIST_ID, 5) + self.rolesListControl = kodigui.ManagedControlList(self, self.ROLES_LIST_ID, 5) + self.progressImageControl = self.getControl(self.PROGRESS_IMAGE_ID) + + self.setup() + self.initialized = True + + self.setFocusId(self.PLAY_BUTTON_ID) + + def onInit(self): + super(ShowWindow, self).onInit() + if self.mediaItem.theme and (not self.cameFrom or self.cameFrom != self.mediaItem.ratingKey) \ + and not util.getSetting("slow_connection", False): + self.cameFrom = self.mediaItem.ratingKey + volume = self.mediaItem.settings.getThemeMusicValue() + if volume > 0: + player.PLAYER.playBackgroundMusic(self.mediaItem.theme.asURL(True), volume, + self.mediaItem.ratingKey) + + def setup(self): + self.mediaItem.reload(includeExtras=1, includeExtrasCount=10) + + self.relatedPaginator = RelatedPaginator(self.relatedListControl, leaf_count=int(self.mediaItem.relatedCount), + parent_window=self) + + self.updateProperties() + self.setBoolProperty("initialized", True) + self.fill() + hasPrev = self.fillExtras() + hasPrev = self.fillRelated(hasPrev) + self.fillRoles(hasPrev) + + def updateProperties(self): + self.setProperty('title', self.mediaItem.title) + self.setProperty('summary', self.mediaItem.summary) + self.setProperty('thumb', self.mediaItem.defaultThumb.asTranscodedImageURL(*self.THUMB_DIMS[self.mediaItem.type]['main.thumb'])) + self.updateBackgroundFrom(self.mediaItem) + self.setProperty('duration', util.durationToText(self.mediaItem.fixedDuration())) + self.setProperty('info', '') + self.setProperty('date', self.mediaItem.year) + + self.setProperty('extras.header', T(32305, 'Extras')) + self.setProperty('related.header', T(32306, 'Related Shows')) + + if self.mediaItem.creator: + self.setProperty('directors', u'{0} {1}'.format(T(32418, 'Creator').upper(), self.mediaItem.creator)) + elif self.mediaItem.studio: + self.setProperty('directors', u'{0} {1}'.format(T(32386, 'Studio').upper(), self.mediaItem.studio)) + + cast = u' / '.join([r.tag for r in self.mediaItem.roles()][:5]) + castLabel = T(32419, 'Cast').upper() + self.setProperty('writers', cast and u'{0} {1}'.format(castLabel, cast) or '') + + genres = self.mediaItem.genres() + self.setProperty('info', genres and (u' / '.join([g.tag for g in genres][:3])) or '') + + self.setProperties(('rating.stars', 'rating', 'rating.image', 'rating2', 'rating2.image'), '') + + if self.mediaItem.userRating: + stars = str(int(round((self.mediaItem.userRating.asFloat() / 10) * 5))) + self.setProperty('rating.stars', stars) + + if self.mediaItem.ratingImage: + rating = self.mediaItem.rating + audienceRating = self.mediaItem.audienceRating + if self.mediaItem.ratingImage.startswith('rottentomatoes:'): + rating = '{0}%'.format(int(rating.asFloat() * 10)) + if audienceRating: + audienceRating = '{0}%'.format(int(audienceRating.asFloat() * 10)) + + self.setProperty('rating', rating) + self.setProperty('rating.image', 'script.plex/ratings/{0}.png'.format(self.mediaItem.ratingImage.replace('://', '/'))) + if self.mediaItem.audienceRatingImage: + self.setProperty('rating2', audienceRating) + self.setProperty('rating2.image', 'script.plex/ratings/{0}.png'.format(self.mediaItem.audienceRatingImage.replace('://', '/'))) + else: + self.setProperty('rating', self.mediaItem.rating) + + sas = self.mediaItem.selectedAudioStream() + self.setProperty('audio', sas and sas.getTitle() or 'None') + + sss = self.mediaItem.selectedSubtitleStream( + forced_subtitles_override=util.getSetting("forced_subtitles_override", False)) + self.setProperty('subtitles', sss and sss.getTitle() or 'None') + + leafcount = self.mediaItem.leafCount.asFloat() + if leafcount: + width = (int((self.mediaItem.viewedLeafCount.asInt() / leafcount) * self.width)) or 1 + self.progressImageControl.setWidth(width) + + def onAction(self, action): + try: + controlID = self.getFocusId() + + if not controlID and self.lastFocusID and not action == xbmcgui.ACTION_MOUSE_MOVE: + self.setFocusId(self.lastFocusID) + + if action == xbmcgui.ACTION_CONTEXT_MENU: + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + self.lastNonOptionsFocusID = self.lastFocusID + self.setFocusId(self.OPTIONS_GROUP_ID) + return + else: + if self.lastNonOptionsFocusID: + self.setFocusId(self.lastNonOptionsFocusID) + self.lastNonOptionsFocusID = None + return + + elif action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format( + self.OPTIONS_GROUP_ID)) and \ + (not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): + if self.getProperty('on.extras'): + self.setFocusId(self.OPTIONS_GROUP_ID) + return + + if action == xbmcgui.ACTION_LAST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + next(self) + elif action == xbmcgui.ACTION_NEXT_ITEM: + self.setFocusId(300) + next(self) + elif action == xbmcgui.ACTION_FIRST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + self.prev() + elif action == xbmcgui.ACTION_PREV_ITEM: + self.setFocusId(300) + self.prev() + + if action == xbmcgui.ACTION_MOVE_UP and (controlID == self.SUB_ITEM_LIST_ID or + self.INFO_BUTTON_ID <= controlID <= self.OPTIONS_BUTTON_ID): + self.updateBackgroundFrom(self.mediaItem) + + if controlID == self.RELATED_LIST_ID: + if self.relatedPaginator.boundaryHit: + self.relatedPaginator.paginate() + return + elif action in (xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT): + self.updateBackgroundFrom(self.relatedListControl.getSelectedItem().dataSource) + + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.SUB_ITEM_LIST_ID: + self.subItemListClicked() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.EXTRA_LIST_ID: + self.openItem(self.extraListControl) + elif controlID == self.RELATED_LIST_ID: + self.openItem(self.relatedListControl) + elif controlID == self.ROLES_LIST_ID: + self.roleClicked() + elif controlID == self.INFO_BUTTON_ID: + self.infoButtonClicked() + elif controlID == self.PLAY_BUTTON_ID: + self.playButtonClicked() + elif controlID == self.SHUFFLE_BUTTON_ID: + self.shuffleButtonClicked() + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked() + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def onFocus(self, controlID): + self.lastFocusID = controlID + + if 399 < controlID < 500: + self.setProperty('hub.focus', str(controlID - 400)) + + if controlID == self.RELATED_LIST_ID: + self.updateBackgroundFrom(self.relatedListControl.getSelectedItem().dataSource) + + if xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + ControlGroup(300).HasFocus(0)'): + self.setProperty('on.extras', '') + elif xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + !ControlGroup(300).HasFocus(0)'): + self.setProperty('on.extras', '1') + + if player.PLAYER.bgmPlaying and player.PLAYER.handler.currentlyPlaying != self.mediaItem.ratingKey: + player.PLAYER.stopAndWait() + + def getMediaItems(self): + return False + + def next(self): + if not self._next(): + return + self.setup() + + __next__ = next + + @busy.dialog() + def _next(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.mediaItem) + if not mli: + return False + + pos = mli.pos() + 1 + if not self.parentList.positionIsValid(pos): + pos = 0 + + self.mediaItem = self.parentList.getListItem(pos).dataSource + else: + if not self.getMediaItems(): + return False + + if self.mediaItem not in self.mediaItems: + return False + + pos = self.mediaItems.index(self.mediaItem) + pos += 1 + if pos >= len(self.mediaItems): + pos = 0 + + self.mediaItem = self.mediaItems[pos] + + return True + + def prev(self): + if not self._prev(): + return + self.setup() + + @busy.dialog() + def _prev(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.mediaItem) + if not mli: + return False + + pos = mli.pos() - 1 + if pos < 0: + pos = self.parentList.size() - 1 + + self.mediaItem = self.parentList.getListItem(pos).dataSource + else: + if not self.getMediaItems(): + return False + + if self.mediaItem not in self.mediaItems: + return False + + pos = self.mediaItems.index(self.mediaItem) + pos -= 1 + if pos < 0: + pos = len(self.mediaItems) - 1 + + self.mediaItem = self.mediaItems[pos] + + return True + + def searchButtonClicked(self): + self.processCommand(search.dialog(self, section_id=self.mediaItem.getLibrarySectionId() or None)) + + def openItem(self, control=None, item=None): + if not item: + mli = control.getSelectedItem() + if not mli: + return + item = mli.dataSource + + self.processCommand(opener.open(item)) + + def subItemListClicked(self): + mli = self.subItemListControl.getSelectedItem() + if not mli: + return + + update = False + + w = None + if self.mediaItem.type == 'show': + w = episodes.EpisodesWindow.open(season=mli.dataSource, show=self.mediaItem, parent_list=self.subItemListControl) + update = True + elif self.mediaItem.type == 'artist': + w = tracks.AlbumWindow.open(album=mli.dataSource, parent_list=self.subItemListControl) + + if not mli: + return + + if not mli.dataSource.exists(): + self.subItemListControl.removeItem(mli.pos()) + + if not self.subItemListControl.size(): + self.closeWithCommand(w.exitCommand) + del w + gc.collect(2) + return + + if update: + mli.setProperty('unwatched.count', not mli.dataSource.isWatched and str(mli.dataSource.unViewedLeafCount) or '') + self.mediaItem.reload(includeRelated=1, includeRelatedCount=10, includeExtras=1, includeExtrasCount=10) + self.updateProperties() + + try: + self.processCommand(w.exitCommand) + finally: + del w + gc.collect(2) + + def infoButtonClicked(self): + fallback = 'script.plex/thumb_fallbacks/{0}.png'.format(self.mediaItem.type == 'show' and 'show' or 'music') + genres = u' / '.join([g.tag for g in util.removeDups(self.mediaItem.genres())][:6]) + + w = info.InfoWindow.open( + title=self.mediaItem.title, + sub_title=genres, + thumb=self.mediaItem.defaultThumb, + thumb_fallback=fallback, + info=self.mediaItem.summary, + background=self.getProperty('background'), + is_square=bool(isinstance(self, ArtistWindow)), + video=self.mediaItem + ) + del w + util.garbageCollect() + + def playButtonClicked(self, shuffle=False): + items = self.mediaItem.all() + pl = playlist.LocalPlaylist(items, self.mediaItem.getServer()) + resume = False + if not shuffle and self.mediaItem.type == 'show': + resume = self.getPlaylistResume(pl, items, self.mediaItem.title) + if resume is None: + return + + pl.shuffle(shuffle, first=True) + videoplayer.play(play_queue=pl, resume=resume) + + def shuffleButtonClicked(self): + self.playButtonClicked(shuffle=True) + + def optionsButtonClicked(self): + options = [] + oldBingeModeValue = None + if xbmc.getCondVisibility('Player.HasAudio + MusicPlayer.HasNext'): + options.append({'key': 'play_next', 'display': 'Play Next'}) + + if self.mediaItem.type != 'artist': + if self.mediaItem.isWatched: + options.append({'key': 'mark_unwatched', 'display': T(32318, 'Mark Unplayed')}) + else: + options.append({'key': 'mark_watched', 'display': T(32319, 'Mark Played')}) + + if self.mediaItem.type == "show": + if options: + options.append(dropdown.SEPARATOR) + + options.append({'key': 'playback_settings', 'display': T(32925, 'Playback Settings')}) + + # if xbmc.getCondVisibility('Player.HasAudio') and self.section.TYPE == 'artist': + # options.append({'key': 'add_to_queue', 'display': 'Add To Queue'}) + + # if False: + # options.append({'key': 'add_to_playlist', 'display': 'Add To Playlist'}) + + options.append(dropdown.SEPARATOR) + + options.append({'key': 'to_section', 'display': u'Go to {0}'.format(self.mediaItem.getLibrarySectionTitle())}) + pos = (880, 618) + + choice = dropdown.showDropdown(options, pos, close_direction='left') + if not choice: + return + + if choice['key'] == 'play_next': + xbmc.executebuiltin('PlayerControl(Next)') + elif choice['key'] == 'mark_watched': + self.mediaItem.markWatched() + self.updateItems() + self.updateProperties() + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'mark_unwatched': + self.mediaItem.markUnwatched() + self.updateItems() + self.updateProperties() + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'to_section': + self.goHome(self.mediaItem.getLibrarySectionId()) + elif choice['key'] == 'playback_settings': + self.playbackSettings(self.mediaItem, pos, False) + + def roleClicked(self): + mli = self.rolesListControl.getSelectedItem() + if not mli: + return + + sectionRoles = busy.widthDialog(mli.dataSource.sectionRoles, '') + + if not sectionRoles: + util.DEBUG_LOG('No sections found for actor') + return + + if len(sectionRoles) > 1: + x, y = self.getRoleItemDDPosition() + + options = [{'role': r, 'display': r.reasonTitle} for r in sectionRoles] + choice = dropdown.showDropdown(options, (x, y), pos_is_bottom=True, close_direction='bottom') + + if not choice: + return + + role = choice['role'] + else: + role = sectionRoles[0] + + self.processCommand(opener.open(role)) + + def getRoleItemDDPosition(self): + y = 980 + if xbmc.getCondVisibility('Control.IsVisible(500)'): + y += 360 + if xbmc.getCondVisibility('Control.IsVisible(501)'): + y += 360 + if xbmc.getCondVisibility('Control.IsVisible(502)'): + y += 520 + if xbmc.getCondVisibility('!String.IsEmpty(Window.Property(on.extras))'): + y -= 300 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),0) + Control.IsVisible(500)'): + y -= 500 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),1) + Control.IsVisible(501)'): + y -= 360 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),1) + Control.IsVisible(502)'): + y -= 500 + + focus = int(xbmc.getInfoLabel('Container(403).Position')) + + x = ((focus + 1) * 304) - 100 + return x, y + + def updateItems(self): + self.fill(update=True) + + def createListItem(self, obj): + mli = kodigui.ManagedListItem( + obj.title or '', + thumbnailImage=obj.defaultThumb.asTranscodedImageURL(*self.THUMB_DIMS[self.mediaItem.type]['item.thumb']), + data_source=obj + ) + return mli + + @busy.dialog() + def fill(self, update=False): + self.fillSeasons(self.mediaItem, update=update) + + def fillExtras(self): + items = [] + idx = 0 + + if not self.mediaItem.extras: + self.extraListControl.reset() + return False + + for extra in self.mediaItem.extras(): + mli = kodigui.ManagedListItem( + extra.title or '', + metadata.EXTRA_MAP.get(extra.extraType.asInt(), ''), + thumbnailImage=extra.thumb.asTranscodedImageURL(*self.EXTRA_DIM), + data_source=extra + ) + + if mli: + mli.setProperty('index', str(idx)) + mli.setProperty( + 'thumb.fallback', 'script.plex/thumb_fallbacks/{0}.png'.format(extra.type in ('show', 'season', 'episode') and 'show' or 'movie') + ) + items.append(mli) + idx += 1 + + if not items: + return False + + self.extraListControl.reset() + self.extraListControl.addItems(items) + return True + + def fillRelated(self, has_prev=False): + if not self.relatedPaginator.leafCount: + self.relatedListControl.reset() + return has_prev + + self.setProperty('divider.{0}'.format(self.RELATED_LIST_ID), has_prev and '1' or '') + + items = self.relatedPaginator.paginate() + + if not items: + return False + + return True + + def fillRoles(self, has_prev=False): + items = [] + idx = 0 + if not self.mediaItem.roles: + self.rolesListControl.reset() + return has_prev + + self.setProperty('divider.{0}'.format(self.ROLES_LIST_ID), has_prev and '1' or '') + + for role in self.mediaItem.roles(): + mli = kodigui.ManagedListItem(role.tag, role.role, thumbnailImage=role.thumb.asTranscodedImageURL(*self.ROLES_DIM), data_source=role) + mli.setProperty('index', str(idx)) + items.append(mli) + idx += 1 + + self.rolesListControl.reset() + self.rolesListControl.addItems(items) + return True + + +class ArtistWindow(ShowWindow): + xmlFile = 'script-plex-artist.xml' + + SUB_ITEM_LIST_ID = 101 + + def onFirstInit(self): + self.subItemListControl = kodigui.ManagedControlList(self, self.SUB_ITEM_LIST_ID, 5) + + self.setup() + + self.setFocusId(self.PLAY_BUTTON_ID) + + def setup(self): + self.updateProperties() + self.fill() + + def playButtonClicked(self, shuffle=False): + pl = playlist.LocalPlaylist(self.mediaItem.all(), self.mediaItem.getServer(), self.mediaItem) + pl.startShuffled = shuffle + w = musicplayer.MusicPlayerWindow.open(track=pl.current(), playlist=pl) + del w + + def updateProperties(self): + self.setProperty('summary', self.mediaItem.summary) + self.setProperty('thumb', self.mediaItem.defaultThumb.asTranscodedImageURL(*self.THUMB_DIMS[self.mediaItem.type]['main.thumb'])) + self.updateBackgroundFrom(self.mediaItem) + + @busy.dialog() + def fill(self): + self.mediaItem.reload(includeRelated=1, includeRelatedCount=10, includeExtras=1, includeExtrasCount=10) + self.setProperty('artist.title', self.mediaItem.title) + genres = u' / '.join([g.tag for g in util.removeDups(self.mediaItem.genres())][:6]) + self.setProperty('artist.genre', genres) + items = [] + idx = 0 + for album in sorted(self.mediaItem.albums(), key=lambda x: x.year): + mli = self.createListItem(album) + if mli: + mli.setProperty('index', str(idx)) + mli.setProperty('year', album.year) + mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/music.png') + items.append(mli) + idx += 1 + + self.subItemListControl.reset() + self.subItemListControl.addItems(items) diff --git a/script.plexmod/lib/windows/tracks.py b/script.plexmod/lib/windows/tracks.py new file mode 100644 index 000000000..90dc43043 --- /dev/null +++ b/script.plexmod/lib/windows/tracks.py @@ -0,0 +1,348 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from . import kodigui + +from lib import colors +from lib import util + +from plexnet import playlist + +from . import busy +from . import musicplayer +from . import dropdown +from . import windowutils +from . import opener +from . import search + +from lib.util import T + + +class AlbumWindow(kodigui.ControlledWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-album.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + THUMB_AR16X9_DIM = util.scaleResolution(178, 100) + THUMB_SQUARE_DIM = util.scaleResolution(630, 630) + + TRACKS_LIST_ID = 101 + LIST_OPTIONS_BUTTON_ID = 111 + + OPTIONS_GROUP_ID = 200 + + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + PLAYER_STATUS_BUTTON_ID = 204 + + PLAY_BUTTON_ID = 301 + SHUFFLE_BUTTON_ID = 302 + OPTIONS_BUTTON_ID = 303 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + self.album = kwargs.get('album') + self.parentList = kwargs.get('parentList') + self.albums = None + self.exitCommand = None + + def onFirstInit(self): + self.trackListControl = kodigui.ManagedControlList(self, self.TRACKS_LIST_ID, 5) + + self.setup() + self.setFocusId(self.TRACKS_LIST_ID) + try: + self.checkForHeaderFocus(xbmcgui.ACTION_MOVE_DOWN) + except AttributeError: + raise util.NoDataException + + def setup(self): + self.updateProperties() + self.fillTracks() + + def onAction(self, action): + controlID = self.getFocusId() + try: + if action == xbmcgui.ACTION_LAST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + self.next() + elif action == xbmcgui.ACTION_NEXT_ITEM: + self.next() + elif action == xbmcgui.ACTION_FIRST_PAGE and xbmc.getCondVisibility('ControlGroup(300).HasFocus(0)'): + self.prev() + elif action == xbmcgui.ACTION_PREV_ITEM: + self.prev() + + if controlID == self.TRACKS_LIST_ID: + self.checkForHeaderFocus(action) + if controlID == self.LIST_OPTIONS_BUTTON_ID and self.checkOptionsAction(action): + return + elif action == xbmcgui.ACTION_CONTEXT_MENU: + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + self.setFocusId(self.OPTIONS_GROUP_ID) + return + # elif action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + # if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + # self.setFocusId(self.OPTIONS_GROUP_ID) + # return + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def checkOptionsAction(self, action): + if action == xbmcgui.ACTION_MOVE_UP: + mli = self.trackListControl.getSelectedItem() + if not mli: + return False + pos = mli.pos() - 1 + if self.trackListControl.positionIsValid(pos): + self.setFocusId(self.TRACKS_LIST_ID) + self.trackListControl.selectItem(pos) + return True + elif action == xbmcgui.ACTION_MOVE_DOWN: + mli = self.trackListControl.getSelectedItem() + if not mli: + return False + pos = mli.pos() + 1 + if self.trackListControl.positionIsValid(pos): + self.setFocusId(self.TRACKS_LIST_ID) + self.trackListControl.selectItem(pos) + return True + + return False + + def onClick(self, controlID): + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.TRACKS_LIST_ID: + self.trackPanelClicked() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.PLAY_BUTTON_ID: + self.playButtonClicked() + elif controlID == self.SHUFFLE_BUTTON_ID: + self.shuffleButtonClicked() + elif controlID == self.OPTIONS_BUTTON_ID: + self.optionsButtonClicked() + elif controlID == self.LIST_OPTIONS_BUTTON_ID: + mli = self.trackListControl.getSelectedItem() + if mli: + self.optionsButtonClicked(mli) + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def getAlbums(self): + if not self.albums: + self.albums = self.album.artist().albums() + + if not self.albums: + return False + + return True + + def next(self): + if not self._next(): + return + self.setup() + + @busy.dialog() + def _next(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.album) + if not mli: + return False + + pos = mli.pos() + 1 + if not self.parentList.positionIsValid(pos): + pos = 0 + + self.album = self.parentList.getListItem(pos).dataSource + else: + if not self.getAlbums(): + return False + + if self.album not in self.albums: + return False + + pos = self.albums.index(self.album) + pos += 1 + if pos >= len(self.albums): + pos = 0 + + self.album = self.albums[pos] + + return True + + def prev(self): + if not self._prev(): + return + self.setup() + + @busy.dialog() + def _prev(self): + if self.parentList: + mli = self.parentList.getListItemByDataSource(self.album) + if not mli: + return False + + pos = mli.pos() - 1 + if pos < 0: + pos = self.parentList.size() - 1 + + self.album = self.parentList.getListItem(pos).dataSource + else: + if not self.getAlbums(): + return False + + if self.album not in self.albums: + return False + + pos = self.albums.index(self.album) + pos -= 1 + if pos < 0: + pos = len(self.albums) - 1 + + self.album = self.albums[pos] + + return True + + def searchButtonClicked(self): + self.processCommand(search.dialog(self, section_id=self.album.getLibrarySectionId() or None)) + + def shuffleButtonClicked(self): + self.playButtonClicked(shuffle=True) + + def optionsButtonClicked(self, item=None): + options = [] + + if item: + if item.dataSource.isWatched: + options.append({'key': 'mark_unwatched', 'display': T(32318, 'Mark Unplayed')}) + else: + options.append({'key': 'mark_watched', 'display': T(32319, 'Mark Played')}) + + # if False: + # options.append({'key': 'add_to_playlist', 'display': '[COLOR FF808080]Add To Playlist[/COLOR]'}) + else: + if xbmc.getCondVisibility('Player.HasAudio + MusicPlayer.HasNext'): + options.append({'key': 'play_next', 'display': T(32325, 'Play Next')}) + + # if xbmc.getCondVisibility('Player.HasAudio') and self.section.TYPE == 'artist': + # options.append({'key': 'add_to_queue', 'display': 'Add To Queue'}) + + if options: + options.append(dropdown.SEPARATOR) + + options.append({'key': 'to_artist', 'display': T(32301, 'Go to Artist')}) + options.append({'key': 'to_section', 'display': T(32302, u'Go to {0}').format(self.album.getLibrarySectionTitle())}) + + pos = (460, 1106) + bottom = True + setDropdownProp = False + if item: + viewPos = self.trackListControl.getViewPosition() + if viewPos > 6: + pos = (1490, 312 + (viewPos * 100)) + bottom = True + else: + pos = (1490, 167 + (viewPos * 100)) + bottom = False + setDropdownProp = True + choice = dropdown.showDropdown(options, pos, pos_is_bottom=bottom, close_direction='right', set_dropdown_prop=setDropdownProp) + if not choice: + return + + if choice['key'] == 'play_next': + xbmc.executebuiltin('PlayerControl(Next)') + elif choice['key'] == 'mark_watched': + media = item and item.dataSource or self.album + media.markWatched() + self.updateItems(item) + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'mark_unwatched': + media = item and item.dataSource or self.album + media.markUnwatched() + self.updateItems(item) + util.MONITOR.watchStatusChanged() + elif choice['key'] == 'to_artist': + self.processCommand(opener.open(self.album.parentRatingKey)) + elif choice['key'] == 'to_section': + self.goHome(self.album.getLibrarySectionId()) + + def checkForHeaderFocus(self, action): + if action in (xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_PAGE_UP): + if self.trackListControl.getSelectedItem().getProperty('is.header'): + xbmc.executebuiltin('Action(up)') + if action in (xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_PAGE_DOWN, xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT): + if self.trackListControl.getSelectedItem().getProperty('is.header'): + xbmc.executebuiltin('Action(down)') + + def updateItems(self, item=None): + if item: + self.album.reload() + item.setProperty('watched', item.dataSource.isWatched and '1' or '') + else: + self.fillTracks() + + def playButtonClicked(self, shuffle=False): + pl = playlist.LocalPlaylist(self.album.all(), self.album.getServer()) + pl.startShuffled = shuffle + self.openWindow(musicplayer.MusicPlayerWindow, track=pl.current(), playlist=pl) + + def trackPanelClicked(self): + mli = self.trackListControl.getSelectedItem() + if not mli: + return + + self.openWindow(musicplayer.MusicPlayerWindow, track=mli.dataSource, album=self.album) + + def updateProperties(self): + self.setProperty( + 'background', + util.backgroundFromArt(self.album.art, width=self.width, height=self.height) + ) + self.setProperty('album.thumb', self.album.thumb.asTranscodedImageURL(*self.THUMB_SQUARE_DIM)) + self.setProperty('artist.title', self.album.parentTitle or '') + self.setProperty('album.title', self.album.title) + + def createListItem(self, obj): + mli = kodigui.ManagedListItem(obj.title, data_source=obj) + mli.setProperty('track.number', str(obj.index) or '') + mli.setProperty('track.duration', util.simplifiedTimeDisplay(obj.duration.asInt())) + return mli + + #@busy.dialog() + def fillTracks(self): + items = [] + idx = 0 + multiDisc = 0 + + for track in self.album.tracks(): + disc = track.parentIndex.asInt() + if disc > 1: + if not multiDisc: + items.insert(0, kodigui.ManagedListItem(u'{0} 1'.format(T(32420, 'Disc').upper()), properties={'is.header': '1'})) + + if disc != multiDisc: + items[-1].setProperty('is.footer', '1') + multiDisc = disc + items.append(kodigui.ManagedListItem('{0} {1}'.format(T(32420, 'Disc').upper(), disc), properties={'is.header': '1'})) + + mli = self.createListItem(track) + if mli: + mli.setProperty('track.ID', track.ratingKey) + mli.setProperty('index', str(idx)) + mli.setProperty('artist', self.album.parentTitle) + mli.setProperty('disc', str(disc)) + mli.setProperty('album', self.album.title) + mli.setProperty('number', '{0:0>2}'.format(track.index)) + items.append(mli) + idx += 1 + + if items: + items[-1].setProperty('is.footer', '1') + + self.trackListControl.replaceItems(items) diff --git a/script.plexmod/lib/windows/userselect.py b/script.plexmod/lib/windows/userselect.py new file mode 100644 index 000000000..c659276a9 --- /dev/null +++ b/script.plexmod/lib/windows/userselect.py @@ -0,0 +1,234 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui + +from . import kodigui +from . import dropdown +from . import busy + +from lib import util, image, backgroundthread +from plexnet import plexapp + +from lib.util import T + + +class UserThumbTask(backgroundthread.Task): + def setup(self, users, callback): + self.users = users + self.callback = callback + return self + + def run(self): + for user in self.users: + if self.isCanceled(): + return + + thumb, back = image.getImage(user.thumb, user.id) + self.callback(user, thumb, back) + + +class UserSelectWindow(kodigui.BaseWindow): + xmlFile = 'script-plex-user_select.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + USER_LIST_ID = 101 + PIN_ENTRY_GROUP_ID = 400 + SHUTDOWN_BUTTON_ID = 500 + + def __init__(self, *args, **kwargs): + self.task = None + self.selected = False + kodigui.BaseWindow.__init__(self, *args, **kwargs) + + def onFirstInit(self): + self.userList = kodigui.ManagedControlList(self, self.USER_LIST_ID, 6) + + self.start() + + def onAction(self, action): + try: + ID = action.getId() + if 57 < ID < 68: + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)): + item = self.userList.getSelectedItem() + if not item.dataSource.isProtected: + return + self.setFocusId(self.PIN_ENTRY_GROUP_ID) + self.pinEntryClicked(ID + 142) + return + elif 142 <= ID <= 149: # JumpSMS action + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)): + item = self.userList.getSelectedItem() + if not item.dataSource.isProtected: + return + self.setFocusId(self.PIN_ENTRY_GROUP_ID) + self.pinEntryClicked(ID + 60) + return + elif ID in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_BACKSPACE): + item = self.userList.getSelectedItem() + if xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)): + if item.getProperty('editing.pin'): + self.pinEntryClicked(211) + else: + self.setFocusId(self.USER_LIST_ID) + return + # return to selected user + self.selected = 'cancel' + self.doClose() + return + except: + util.ERROR() + + kodigui.BaseWindow.onAction(self, action) + + def onClick(self, controlID): + if controlID == self.USER_LIST_ID: + item = self.userList.getSelectedItem() + if item.dataSource.isProtected: + self.setFocusId(self.PIN_ENTRY_GROUP_ID) + else: + self.userSelected(item) + elif 200 < controlID < 212: + self.pinEntryClicked(controlID) + elif controlID == self.SHUTDOWN_BUTTON_ID: + self.shutdownClicked() + + def onFocus(self, controlID): + if controlID == self.USER_LIST_ID: + item = self.userList.getSelectedItem() + item.setProperty('editing.pin', '') + + def userThumbCallback(self, user, thumb, back): + item = self.userList.getListItemByDataSource(user) + if item: + item.setThumbnailImage(thumb) + item.setProperty('back.image', back) + + def start(self): + self.setProperty('busy', '1') + try: + users = plexapp.ACCOUNT.homeUsers + + items = [] + selectIndex = None + for idx, user in enumerate(users): + # thumb, back = image.getImage(user.thumb, user.id) + # mli = kodigui.ManagedListItem(user.title, thumbnailImage=thumb, data_source=user) + mli = kodigui.ManagedListItem(user.title, user.title[0].upper(), data_source=user) + mli.setProperty('pin', user.title) + # mli.setProperty('back.image', back) + mli.setProperty('protected', user.isProtected and '1' or '') + mli.setProperty('admin', user.isAdmin and '1' or '') + + if plexapp.ACCOUNT.ID == user.id: + selectIndex = idx + + items.append(mli) + + self.userList.addItems(items) + + self.task = UserThumbTask().setup(users, self.userThumbCallback) + backgroundthread.BGThreader.addTask(self.task) + + self.setFocusId(self.USER_LIST_ID) + + if selectIndex is not None: + self.userList.setSelectedItemByPos(selectIndex) + + self.setProperty('initialized', '1') + finally: + self.setProperty('busy', '') + + def shutdownClicked(self): + options = [] + options.append({'key': 'sign_out', 'display': T(32421, 'Sign Out')}) + options.append({'key': 'exit', 'display': T(32422, 'Exit')}) + if util.getSetting('kiosk.mode', False): + if xbmc.getCondVisibility('System.CanPowerDown'): + options.append({'key': 'shutdown', 'display': T(32423, 'Shutdown')}) + if xbmc.getCondVisibility('System.CanSuspend'): + options.append({'key': 'suspend', 'display': T(32424, 'Suspend')}) + if xbmc.getCondVisibility('System.CanHibernate'): + options.append({'key': 'hibernate', 'display': T(32425, 'Hibernate')}) + if xbmc.getCondVisibility('System.CanReboot'): + options.append({'key': 'reboot', 'display': T(32426, 'Reboot')}) + + with self.propertyContext('dropdown'): + choice = dropdown.showDropdown(options, (60, 101)) + if not choice: + return + + if choice['key'] == 'sign_out': + self.selected = 'signout' + self.doClose() + elif choice['key'] == 'exit': + self.doClose() + elif choice['key'] == 'shutdown': + xbmc.executebuiltin('Powerdown()') + elif choice['key'] == 'suspend': + xbmc.executebuiltin('Suspend()') + elif choice['key'] == 'hibernate': + xbmc.executebuiltin('Hibernate()') + elif choice['key'] == 'reboot': + xbmc.executebuiltin('Reset()') + + def pinEntryClicked(self, controlID): + item = self.userList.getSelectedItem() + if item.getProperty('editing.pin'): + pin = item.getProperty('editing.pin') + else: + pin = '' + + if len(pin) > 3: + return + + if controlID < 210: + pin += str(controlID - 200) + elif controlID == 210: + pin += '0' + elif controlID == 211: + pin = pin[:-1] + + if pin: + item.setProperty('pin', ' '.join(list(u"\u2022" * len(pin)))) + item.setProperty('editing.pin', pin) + if len(pin) > 3: + self.userSelected(item, pin) + else: + item.setProperty('pin', item.dataSource.title) + item.setProperty('editing.pin', '') + + @busy.dialog() + def userSelected(self, item, pin=None): + user = item.dataSource + # xbmc.sleep(500) + util.DEBUG_LOG('Home user selected: {0}'.format(user)) + + from lib import plex + with plex.CallbackEvent(plexapp.util.APP, 'account:response') as e: + if plexapp.ACCOUNT.switchHomeUser(user.id, pin) and plexapp.ACCOUNT.switchUser: + util.DEBUG_LOG('Waiting for user change...') + else: + e.close() + item.setProperty('pin', item.dataSource.title) + item.setProperty('editing.pin', '') + util.messageDialog(T(32427, 'Failed'), T(32926, 'Wrong pin entered!')) + return + + self.selected = True + self.doClose() + + def finished(self): + if self.task: + self.task.cancel() + + +def start(): + w = UserSelectWindow.open() + selected = w.selected + del w + return selected diff --git a/script.plexmod/lib/windows/videoplayer.py b/script.plexmod/lib/windows/videoplayer.py new file mode 100644 index 000000000..0e36426cd --- /dev/null +++ b/script.plexmod/lib/windows/videoplayer.py @@ -0,0 +1,596 @@ +from __future__ import absolute_import +import time +import threading +import math + +from kodi_six import xbmc +from kodi_six import xbmcgui + +from . import kodigui +from . import windowutils +from . import opener +from . import busy +from . import search +from . import dropdown +from . import pagination + +from lib import util +from lib import player +from lib import colors + +from lib.util import T + + +PASSOUT_PROTECTION_DURATION_SECONDS = 7200 +PASSOUT_LAST_VIDEO_DURATION_MILLIS = 1200000 + + +class RelatedPaginator(pagination.BaseRelatedPaginator): + def readyForPaging(self): + return self.parentWindow.postPlayInitialized + + def getData(self, offset, amount): + return (self.parentWindow.prev or self.parentWindow.next).getRelated(offset=offset, limit=amount) + + +class OnDeckPaginator(pagination.MCLPaginator): + def readyForPaging(self): + return self.parentWindow.postPlayInitialized + + thumbFallback = lambda self, rel: 'script.plex/thumb_fallbacks/{0}.png'.format( + rel.type in ('show', 'season', 'episode') and 'show' or 'movie') + + def prepareListItem(self, data, mli): + mli.setProperty('progress', util.getProgressImage(mli.dataSource)) + mli.setProperty('unwatched', not mli.dataSource.isWatched and '1' or '') + + if data.type in 'episode': + mli.setLabel2( + u'{0}{1} \u2022 {2}{3}'.format(T(32310, 'S'), data.parentIndex, T(32311, 'E'), data.index)) + else: + mli.setLabel2(data.year) + + def createListItem(self, ondeck): + title = ondeck.grandparentTitle or ondeck.title + if ondeck.type == 'episode': + thumb = ondeck.thumb.asTranscodedImageURL(*self.parentWindow.ONDECK_DIM) + else: + thumb = ondeck.defaultArt.asTranscodedImageURL(*self.parentWindow.ONDECK_DIM) + + mli = kodigui.ManagedListItem(title or '', thumbnailImage=thumb, data_source=ondeck) + if mli: + return mli + + def getData(self, offset, amount): + return (self.parentWindow.prev or self.parentWindow.next).sectionOnDeck(offset=offset, limit=amount) + + +class VideoPlayerWindow(kodigui.ControlledWindow, windowutils.UtilMixin): + xmlFile = 'script-plex-video_player.xml' + path = util.ADDON.getAddonInfo('path') + theme = 'Main' + res = '1080i' + width = 1920 + height = 1080 + + NEXT_DIM = util.scaleResolution(537, 303) + PREV_DIM = util.scaleResolution(462, 259) + ONDECK_DIM = util.scaleResolution(329, 185) + RELATED_DIM = util.scaleResolution(268, 397) + ROLES_DIM = util.scaleResolution(334, 334) + + OPTIONS_GROUP_ID = 200 + + PREV_BUTTON_ID = 101 + NEXT_BUTTON_ID = 102 + + ONDECK_LIST_ID = 400 + RELATED_LIST_ID = 401 + ROLES_LIST_ID = 403 + + HOME_BUTTON_ID = 201 + SEARCH_BUTTON_ID = 202 + + PLAYER_STATUS_BUTTON_ID = 204 + + def __init__(self, *args, **kwargs): + kodigui.ControlledWindow.__init__(self, *args, **kwargs) + windowutils.UtilMixin.__init__(self) + self.playQueue = kwargs.get('play_queue') + self.video = kwargs.get('video') + self.resume = bool(kwargs.get('resume')) + + self.postPlayMode = False + self.prev = None + self.playlist = None + self.handler = None + self.next = None + self.videos = None + self.trailer = None + self.aborted = True + self.timeout = None + self.passoutProtection = 0 + self.postPlayInitialized = False + self.relatedPaginator = None + self.onDeckPaginator = None + self.lastFocusID = None + self.lastNonOptionsFocusID = None + self.playBackStarted = False + + def doClose(self): + util.DEBUG_LOG('VideoPlayerWindow: Closing') + self.timeout = None + self.relatedPaginator = None + self.onDeckPaginator = None + kodigui.ControlledWindow.doClose(self) + player.PLAYER.handler.sessionEnded() + + def onFirstInit(self): + player.PLAYER.on('session.ended', self.sessionEnded) + player.PLAYER.on('av.started', self.playerPlaybackStarted) + player.PLAYER.on('post.play', self.postPlay) + player.PLAYER.on('change.background', self.changeBackground) + + self.onDeckListControl = kodigui.ManagedControlList(self, self.ONDECK_LIST_ID, 5) + self.relatedListControl = kodigui.ManagedControlList(self, self.RELATED_LIST_ID, 5) + self.rolesListControl = kodigui.ManagedControlList(self, self.ROLES_LIST_ID, 5) + + util.DEBUG_LOG('VideoPlayerWindow: Starting session (ID: {0})'.format(id(self))) + self.resetPassoutProtection() + self.play(resume=self.resume) + + def onReInit(self): + self.setBackground() + + def onAction(self, action): + try: + if self.postPlayMode: + controlID = self.getFocusId() + + self.cancelTimer() + self.resetPassoutProtection() + if action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + if not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU: + self.lastNonOptionsFocusID = self.lastFocusID + self.setFocusId(self.OPTIONS_GROUP_ID) + return + else: + if self.lastNonOptionsFocusID and action == xbmcgui.ACTION_CONTEXT_MENU: + self.setFocusId(self.lastNonOptionsFocusID) + self.lastNonOptionsFocusID = None + return + + if action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU): + self.doClose() + return + + if action in (xbmcgui.ACTION_NEXT_ITEM, xbmcgui.ACTION_PLAYER_PLAY): + self.playVideo() + elif action == xbmcgui.ACTION_PREV_ITEM: + self.playVideo(prev=True) + elif action == xbmcgui.ACTION_STOP: + self.doClose() + + if controlID == self.RELATED_LIST_ID: + if self.relatedPaginator.boundaryHit: + self.relatedPaginator.paginate() + return + + elif controlID == self.ONDECK_LIST_ID: + if self.onDeckPaginator.boundaryHit: + self.onDeckPaginator.paginate() + return + except: + util.ERROR() + + kodigui.ControlledWindow.onAction(self, action) + + def playerPlaybackStarted(self, *args, **kwargs): + self.playBackStarted = True + + def onClick(self, controlID): + if not self.postPlayMode: + return + + timeoutCanceled = False + if util.advancedSettings.postplayCancel: + timeoutCanceled = bool(self.timeout) + self.cancelTimer() + + if controlID == self.HOME_BUTTON_ID: + self.goHome() + elif controlID == self.ONDECK_LIST_ID: + self.openItem(self.onDeckListControl) + elif controlID == self.RELATED_LIST_ID: + self.openItem(self.relatedListControl) + elif controlID == self.ROLES_LIST_ID: + self.roleClicked() + elif controlID == self.PREV_BUTTON_ID: + self.playVideo(prev=True) + elif controlID == self.NEXT_BUTTON_ID: + if not timeoutCanceled: + self.playVideo() + elif controlID == self.PLAYER_STATUS_BUTTON_ID: + self.showAudioPlayer() + elif controlID == self.SEARCH_BUTTON_ID: + self.searchButtonClicked() + + def onFocus(self, controlID): + if not self.postPlayMode: + return + + self.lastFocusID = controlID + + if 399 < controlID < 500: + self.setProperty('hub.focus', str(controlID - 400)) + else: + self.setProperty('hub.focus', '') + + if xbmc.getCondVisibility('Control.HasFocus(101) | Control.HasFocus(102) | ControlGroup(200).HasFocus(0)'): + self.setProperty('on.extras', '') + elif xbmc.getCondVisibility('ControlGroup(60).HasFocus(0)'): + self.setProperty('on.extras', '1') + + def searchButtonClicked(self): + self.processCommand(search.dialog(self, section_id=self.prev.getLibrarySectionId() or None)) + + def roleClicked(self): + mli = self.rolesListControl.getSelectedItem() + if not mli: + return + + sectionRoles = busy.widthDialog(mli.dataSource.sectionRoles, '') + + if not sectionRoles: + util.DEBUG_LOG('No sections found for actor') + return + + if len(sectionRoles) > 1: + x, y = self.getRoleItemDDPosition() + + options = [{'role': r, 'display': r.reasonTitle} for r in sectionRoles] + choice = dropdown.showDropdown(options, (x, y), pos_is_bottom=True, close_direction='bottom') + + if not choice: + return + + role = choice['role'] + else: + role = sectionRoles[0] + + self.processCommand(opener.open(role)) + + def getRoleItemDDPosition(self): + y = 1000 + if xbmc.getCondVisibility('Control.IsVisible(500)'): + y += 360 + if xbmc.getCondVisibility('Control.IsVisible(501)'): + y += 520 + if xbmc.getCondVisibility('!String.IsEmpty(Window.Property(on.extras))'): + y -= 300 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),0) + Control.IsVisible(500)'): + y -= 500 + if xbmc.getCondVisibility('Integer.IsGreater(Window.Property(hub.focus),1) + Control.IsVisible(501)'): + y -= 500 + + focus = int(xbmc.getInfoLabel('Container(403).Position')) + + x = ((focus + 1) * 304) - 100 + return x, y + + def setBackground(self): + video = self.video if self.video else self.playQueue.current() + self.windowSetBackground(video.defaultArt.asTranscodedImageURL(1920, 1080, opacity=60, + background=colors.noAlpha.Background)) + + def changeBackground(self, url, **kwargs): + self.windowSetBackground(url) + + def sessionEnded(self, session_id=None, **kwargs): + if session_id != id(self): + util.DEBUG_LOG('VideoPlayerWindow: Ignoring session end (ID: {0} - SessionID: {1})'.format(id(self), session_id)) + return + + util.DEBUG_LOG('VideoPlayerWindow: Session ended - closing (ID: {0})'.format(id(self))) + self.doClose() + + def play(self, resume=False, handler=None): + self.hidePostPlay() + + self.setBackground() + if self.playQueue: + player.PLAYER.playVideoPlaylist(self.playQueue, resume=self.resume, session_id=id(self), handler=handler) + elif self.video: + player.PLAYER.playVideo(self.video, resume=self.resume, force_update=True, session_id=id(self), handler=handler) + + def openItem(self, control=None, item=None): + if not item: + mli = control.getSelectedItem() + if not mli: + return + item = mli.dataSource + + self.processCommand(opener.open(item)) + + def showPostPlay(self): + self.postPlayMode = True + self.setProperty('post.play', '1') + + def hidePostPlay(self): + self.postPlayMode = False + self.setProperty('post.play', '') + self.setProperties(( + 'post.play.background', + 'info.title', + 'info.duration', + 'info.summary', + 'info.date', + 'next.thumb', + 'next.title', + 'next.subtitle', + 'prev.thumb', + 'prev.title', + 'prev.subtitle', + 'related.header', + 'has.next' + ), '') + + self.onDeckListControl.reset() + self.relatedListControl.reset() + self.rolesListControl.reset() + + @busy.dialog() + def postPlay(self, video=None, playlist=None, handler=None, stoppedInBingeMode=False, **kwargs): + util.DEBUG_LOG('VideoPlayer: Starting post-play') + self.showPostPlay() + self.prev = video + self.playlist = playlist + self.handler = handler + + self.getHubs() + + self.setProperty( + 'thumb.fallback', 'script.plex/thumb_fallbacks/{0}.png'.format(self.prev.type in ('show', 'season', 'episode') and 'show' or 'movie') + ) + + util.DEBUG_LOG('PostPlay: Showing video info') + if self.next: + self.next.reload(includeExtras=1, includeExtrasCount=10) + + self.relatedPaginator = RelatedPaginator(self.relatedListControl, + leaf_count=int((self.prev or self.next).relatedCount), + parent_window=self) + + vid = self.prev or self.next + if vid.sectionOnDeckCount: + self.onDeckPaginator = OnDeckPaginator(self.onDeckListControl, + leaf_count=int(vid.sectionOnDeckCount), + parent_window=self) + + self.setInfo() + self.fillOnDeck() + hasPrev = self.fillRelated() + self.fillRoles(hasPrev) + + if not stoppedInBingeMode: + self.startTimer() + + if self.next: + self.setFocusId(self.NEXT_BUTTON_ID) + else: + self.setFocusId(self.PREV_BUTTON_ID) + self.postPlayInitialized = True + + def resetPassoutProtection(self): + self.passoutProtection = time.time() + PASSOUT_PROTECTION_DURATION_SECONDS + + def startTimer(self): + if not util.getUserSetting('post_play_auto', True): + util.DEBUG_LOG('Post play auto-play disabled') + return + + if not self.next: + return + + if time.time() > self.passoutProtection and self.prev.duration.asInt() > PASSOUT_LAST_VIDEO_DURATION_MILLIS: + util.DEBUG_LOG('Post play auto-play skipped: Passout protection') + return + else: + millis = (self.passoutProtection - time.time()) * 1000 + util.DEBUG_LOG('Post play auto-play: Passout protection in {0}'.format(util.durationToShortText(millis))) + + self.timeout = time.time() + abs(util.advancedSettings.postplayTimeout) + util.DEBUG_LOG('Starting post-play timer until: %i' % self.timeout) + threading.Thread(target=self.countdown).start() + + def cancelTimer(self): + if self.timeout is not None: + util.DEBUG_LOG('Canceling post-play timer') + + self.timeout = None + self.setProperty('countdown', '') + + def countdown(self): + while self.timeout and not util.MONITOR.waitForAbort(0.1): + now = time.time() + if self.timeout and now > self.timeout: + self.timeout = None + self.setProperty('countdown', '') + util.DEBUG_LOG('Post-play timer finished') + # This works. The direct method caused the OSD to be broken, possibly because it was triggered from another thread? + # That was the only real difference I could see between the direct method and the user actually clicking the button. + xbmc.executebuiltin('SendClick(,{0})'.format(self.NEXT_BUTTON_ID)) + # Direct method, causes issues with OSD + # self.playVideo() + break + elif self.timeout is not None: + cd = min(abs(util.advancedSettings.postplayTimeout-1), int((self.timeout or now) - now)) + base = 15 / float(util.advancedSettings.postplayTimeout-1) + self.setProperty('countdown', str(int(math.ceil(base*cd)))) + + def getHubs(self): + try: + self.hubs = self.prev.postPlay() + except: + util.ERROR("No data - disconnected?", notify=True, time_ms=5000) + self.doClose() + return + + self.next = None + + if self.playlist: + if self.prev != self.playlist.current(): + self.next = self.playlist.current() + else: + if self.prev.type == 'episode' and 'tv.upnext' in self.hubs: + self.next = self.hubs['tv.upnext'].items[-1] + + if self.next: + self.setProperty('has.next', '1') + + def setInfo(self): + if self.next: + self.setProperty( + 'post.play.background', + util.backgroundFromArt(self.next.art, width=self.width, height=self.height) + ) + self.setProperty('info.title', self.next.title) + self.setProperty('info.duration', util.durationToText(self.next.duration.asInt())) + self.setProperty('info.summary', self.next.summary) + + if self.prev: + self.setProperty( + 'post.play.background', + util.backgroundFromArt(self.prev.art, width=self.width, height=self.height) + ) + self.setProperty('prev.info.title', self.prev.title) + self.setProperty('prev.info.duration', util.durationToText(self.prev.duration.asInt())) + self.setProperty('prev.info.summary', self.prev.summary) + + if self.prev.type == 'episode': + self.setProperty('related.header', T(32306, 'Related Shows')) + if self.next: + self.setProperty('next.thumb', self.next.thumb.asTranscodedImageURL(*self.NEXT_DIM)) + self.setProperty('info.date', util.cleanLeadingZeros(self.next.originallyAvailableAt.asDatetime('%B %d, %Y'))) + + self.setProperty('next.title', self.next.grandparentTitle) + self.setProperty( + 'next.subtitle', u'{0} {1} \u2022 {2} {3}'.format(T(32303, 'Season'), self.next.parentIndex, T(32304, 'Episode'), self.next.index) + ) + if self.prev: + self.setProperty('prev.thumb', self.prev.thumb.asTranscodedImageURL(*self.PREV_DIM)) + self.setProperty('prev.title', self.prev.grandparentTitle) + self.setProperty( + 'prev.subtitle', u'{0} {1} \u2022 {2} {3}'.format(T(32303, 'Season'), self.prev.parentIndex, T(32304, 'Episode'), self.prev.index) + ) + self.setProperty('prev.info.date', util.cleanLeadingZeros(self.prev.originallyAvailableAt.asDatetime('%B %d, %Y'))) + elif self.prev.type == 'movie': + self.setProperty('related.header', T(32404, 'Related Movies')) + if self.next: + self.setProperty('next.thumb', self.next.defaultArt.asTranscodedImageURL(*self.NEXT_DIM)) + self.setProperty('info.date', self.next.year) + + self.setProperty('next.title', self.next.title) + self.setProperty('next.subtitle', self.next.year) + if self.prev: + self.setProperty('prev.thumb', self.prev.defaultArt.asTranscodedImageURL(*self.PREV_DIM)) + self.setProperty('prev.title', self.prev.title) + self.setProperty('prev.subtitle', self.prev.year) + self.setProperty('prev.info.date', self.prev.year) + + def fillOnDeck(self): + if not self.onDeckPaginator: + return False + + if not self.onDeckPaginator.leafCount: + self.onDeckPaginator.reset() + return False + + items = self.onDeckPaginator.paginate() + + if not items: + return False + + return True + + def fillRelated(self, has_prev=False): + if not self.relatedPaginator.leafCount: + self.relatedListControl.reset() + return False + + items = self.relatedPaginator.paginate() + + if not items: + return False + + self.setProperty('divider.{0}'.format(self.RELATED_LIST_ID), has_prev and '1' or '') + return True + + def fillRoles(self, has_prev=False): + items = [] + idx = 0 + + video = self.next if self.next else self.prev + + if not video.roles: + self.rolesListControl.reset() + return False + + for role in video.roles(): + mli = kodigui.ManagedListItem(role.tag, role.role, thumbnailImage=role.thumb.asTranscodedImageURL(*self.ROLES_DIM), data_source=role) + mli.setProperty('index', str(idx)) + items.append(mli) + idx += 1 + + if not items: + return False + + self.setProperty('divider.{0}'.format(self.ROLES_LIST_ID), has_prev and '1' or '') + + self.rolesListControl.reset() + self.rolesListControl.addItems(items) + return True + + def playVideo(self, prev=False): + self.cancelTimer() + try: + if not self.next and self.playlist: + if prev: + self.playlist.prev() + self.aborted = False + self.playQueue = self.playlist + self.video = None + self.play(handler=self.handler) + else: + video = self.next + if prev: + video = self.prev + + if not video: + util.DEBUG_LOG('Trying to play next video with no next video available') + self.video = None + return + + self.playQueue = None + self.video = video + self.play(handler=self.handler) + except: + util.ERROR() + + +def play(video=None, play_queue=None, resume=False): + try: + w = VideoPlayerWindow.open(video=video, play_queue=play_queue, resume=resume) + except util.NoDataException: + raise + finally: + player.PLAYER.off('session.ended', w.sessionEnded) + player.PLAYER.off('post.play', w.postPlay) + player.PLAYER.off('av.started', w.playerPlaybackStarted) + player.PLAYER.off('change.background', w.changeBackground) + player.PLAYER.reset() + command = w.exitCommand + del w + util.garbageCollect() + return command diff --git a/script.plexmod/lib/windows/windowutils.py b/script.plexmod/lib/windows/windowutils.py new file mode 100644 index 000000000..9914d8c87 --- /dev/null +++ b/script.plexmod/lib/windows/windowutils.py @@ -0,0 +1,84 @@ +from __future__ import absolute_import +from lib import util +from . import opener +from . import dropdown + +from lib.util import T + + +HOME = None + + +class UtilMixin(): + def __init__(self): + self.exitCommand = None + + def goHome(self, section=None): + HOME.show() + if section: + self.closeWithCommand('HOME:{0}'.format(section)) + else: + self.closeWithCommand('HOME') + + def openItem(self, obj): + self.processCommand(opener.open(obj)) + + def openWindow(self, window_class, **kwargs): + self.processCommand(opener.handleOpen(window_class, **kwargs)) + + def processCommand(self, command): + if command and command.startswith('HOME'): + self.exitCommand = command + self.doClose() + elif command and command == "NODATA": + raise util.NoDataException + + def closeWithCommand(self, command): + self.exitCommand = command + self.doClose() + + def showAudioPlayer(self, **kwargs): + from . import musicplayer + self.processCommand(opener.handleOpen(musicplayer.MusicPlayerWindow, **kwargs)) + + def getPlaylistResume(self, pl, items, title): + resume = False + watched = False + for i in items: + if (watched and not i.isWatched) or i.get('viewOffset').asInt(): + if i.get('viewOffset'): + choice = dropdown.showDropdown( + options=[ + {'key': 'resume', 'display': T(32429, 'Resume from {0}').format(util.timeDisplay(i.viewOffset.asInt()).lstrip('0').lstrip(':'))}, + {'key': 'play', 'display': T(32317, 'Play from beginning')} + ], + pos=(660, 441), + close_direction='none', + set_dropdown_prop=False, + header=u'{0} - {1}{2} \u2022 {3}{4}'.format(title, T(32310, 'S'), i.parentIndex, T(32311, 'E'), i.index) + ) + + if not choice: + return None + + if choice['key'] == 'resume': + resume = True + + pl.setCurrent(i) + break + elif i.isWatched: + watched = True + else: + break + + return resume + + +def shutdownHome(): + global HOME + if HOME: + HOME._shuttingDown = True + HOME.shutdown() + del HOME + HOME = None + HOME diff --git a/script.plexmod/plugin.py b/script.plexmod/plugin.py new file mode 100644 index 000000000..6e2eec66e --- /dev/null +++ b/script.plexmod/plugin.py @@ -0,0 +1,75 @@ +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcplugin +from kodi_six import xbmcgui +import sys +import base64 +from lib import _included_packages, plex, util +from plexnet import audio, plexplayer, plexapp +from plexnet import util as plexnetUtil + +HANDLE = int(sys.argv[1]) + +BASE_LOG = util.LOG + + +def LOG(msg): + BASE_LOG('(plugin) - {0}'.format(plexnetUtil.cleanToken(msg))) + + +util.LOG = LOG + + +def playTrack(track): + track.reload() + apobj = plexplayer.PlexAudioPlayer(track) + url = apobj.build()['url'] + url = util.addURLParams(url, { + 'X-Plex-Client-Profile-Name': 'Generic', + 'X-Plex-Client-Identifier': plexapp.util.INTERFACE.getGlobal('clientIdentifier') + }) + LOG('Playing URL: {0}'.format(url)) + + return xbmcgui.ListItem(path=url) + + +def playVideo(video): + return None + + +def play(data): + try: + from plexnet import plexobjects + + plexObject = plexobjects.PlexObject.deSerialize(base64.urlsafe_b64decode(data.encode('utf-8'))) + + if plexObject.type == 'track': + listitem = playTrack(plexObject) + elif plexObject.type in ('episode', 'movie', 'clip'): + listitem = playVideo(plexObject) + except: + util.ERROR() + xbmcplugin.setResolvedUrl(HANDLE, False, None) + return + + xbmcplugin.setResolvedUrl(HANDLE, True, listitem) + + +def main(): + try: + if len(sys.argv) < 3: + return + + path = sys.argv[0].split('/', 3)[-1] + data = sys.argv[2].lstrip('?') + + if path == 'play': + play(data) + else: # This is a hack since it's both a plugin and a script. My Addons and Shortcuts otherwise can't launch the add-on + xbmc.executebuiltin('Action(back)') # This sometimes works to back out of the plugin directory display + xbmc.executebuiltin('RunScript(script.plexmod)') + except: + util.ERROR() + + +main() diff --git a/script.plexmod/pm4k_cache_template.xml b/script.plexmod/pm4k_cache_template.xml new file mode 100644 index 000000000..82ad94e32 --- /dev/null +++ b/script.plexmod/pm4k_cache_template.xml @@ -0,0 +1,5 @@ + + 1 + {memorysize} + {readfactor} + \ No newline at end of file diff --git a/script.plexmod/resources/language/resource.language.cs_cz/strings.po b/script.plexmod/resources/language/resource.language.cs_cz/strings.po new file mode 100644 index 000000000..189e635a4 --- /dev/null +++ b/script.plexmod/resources/language/resource.language.cs_cz/strings.po @@ -0,0 +1,927 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2017-02-04 14:36+0100\n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Michal Kuncl \n" +"X-Generator: Poedit 2.0beta2\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Hlavní" + +msgctxt "#32001" +msgid "Original" +msgstr "Originální" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mb/s 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mb/s 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mb/s 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mb/s 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mb/s 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mb/s 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mb/s 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mb/s 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kb/s" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kb/s" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kb/s" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kb/s" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kb/s" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Místní kvalita" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Vzdálená kvalita" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Online kvalita" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Formát překódování" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Ladící záznamy" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Povolit přímé přehrávání" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Povolit přímé streamování" + +msgctxt "#32027" +msgid "Force" +msgstr "Vynutit" + +msgctxt "#32028" +msgid "Always" +msgstr "Vždy" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Pouze obrázkové formáty" + +msgctxt "#32030" +msgid "Auto" +msgstr "Automaticky" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Vpálit titulky (pouze přímá přehrávání)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Povolit nezabezpečené připojení" + +msgctxt "#32033" +msgid "Never" +msgstr "Nikdy" + +msgctxt "#32034" +msgid "On Same network" +msgstr "Pouze na stejné síti" + +msgctxt "#32035" +msgid "Always" +msgstr "Vždy" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Povolit 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Povolit HEVC (H265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Automaticky přihlašovat" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Automatické přehrání po skončení" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Povolit stahování titulků" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Povolit stahování titulků" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Objevení serveru (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Spustit plex při spuštění Kodi" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "IP připojení 1" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Port připojení 1" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "IP připojení 2" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Port připojení 2" + +msgctxt "#32048" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Rozšířené" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Ruční servery" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Soukromí" + +msgctxt "#32052" +msgid "About" +msgstr "O doplňku" + +msgctxt "#32053" +msgid "Video" +msgstr "Video" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Verze doplňku" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Verze Kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Rozlišení obrazovky" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Verze serveru" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Při spuštění přeskočit výběr uživatele a zadávání pinu." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Pokud povolíte, po zkončení přehrávání se automaticky přehraje položka ze seznamu 'Následující' po prodlevě 15 sekund." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Povolte pokud váš hardware zvládne přehrávat 4K videa. Zakázáním vynutíte překódování." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Povolte pokud váš hardware zvládne přehrávat formát HEVC/H265. Zakázáním vynutíte překódování." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Kdy se připojovat k serverům přes nezabezpečené připojení.[CR][CR]* [B]Nikdy[/B]: Nepoužívat nezabezpečená připojení[CR]* [B]Na stejné síti[/B]: Povolte pokud jste na stejné síti jako server[CR]* [B]Vždy[/B]: Povolí na stejné síti a při vzdálených připojeních" + +msgctxt "#32201" +msgid "Trailer" +msgstr "Ukázka" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Smazaná scéna" + +msgctxt "#32203" +msgid "Interview" +msgstr "Rozhovor" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Videoklip" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Ze zákulisí" + +msgctxt "#32206" +msgid "Scene" +msgstr "Scéna" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Živý videoklip" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Videoklip s textem" + +msgctxt "#32209" +msgid "Concert" +msgstr "Koncert" + +msgctxt "#32210" +msgid "Featurette" +msgstr "" + +msgctxt "#32211" +msgid "Short" +msgstr "Krátký film" + +msgctxt "#32212" +msgid "Other" +msgstr "Jiný" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Přejít na album" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Přejít na interpreta" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Přejít na {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Sezóna" + +msgctxt "#32304" +msgid "Episode" +msgstr "Epizoda" + +msgctxt "#32305" +msgid "Extras" +msgstr "Extra" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Podobné seriály" + +msgctxt "#32307" +msgid "More" +msgstr "Více" + +msgctxt "#32308" +msgid "Available" +msgstr "K dispozici" + +msgctxt "#32309" +msgid "None" +msgstr "Žádné" + +msgctxt "#32310" +msgid "S" +msgstr "" + +msgctxt "#32311" +msgid "E" +msgstr "" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Není k dispozici" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Tato položka momentálně není k dispozici" + +msgctxt "#32314" +msgid "In Progress" +msgstr "Rozkoukané" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Pokračovat v přehrávání?" + +msgctxt "#32316" +msgid "Resume" +msgstr "Pokračovat" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Přehrát od začátku" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Označit jako neshlédnuté" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Označit jako shlédnuté" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Označit sezónu jako neshlédnutou" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Označit sezónu jako shlédnutou" + +msgctxt "#32322" +msgid "Delete" +msgstr "Smazat" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Přejít na seriál" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Přejít na {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Přehrát další" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Opravdu smazat?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Opravdu chcete smazat tyto média?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Ano" + +msgctxt "#32329" +msgid "No" +msgstr "Ne" + +msgctxt "#32330" +msgid "Message" +msgstr "Zpráva" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Při mazání média se vyskytla chyba." + +msgctxt "#32332" +msgid "Home" +msgstr "Domů" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Playlisty" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Potvrdit odchod" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Opravdu chcete odejít z Plexu?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Odejít" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Zrušit" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Nenalezeny žádné servery" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Server je nedostupný" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Probíhají zkoušky připojení. Prosím počkejte." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Server je nedostupný. Prosím přihlašte se ke svému serveru a zkontrolujte připojení." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Přepnout uživatele" + +msgctxt "#32343" +msgid "Settings" +msgstr "Nastavení" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Odhlásit" + +msgctxt "#32345" +msgid "All" +msgstr "Všechno" + +msgctxt "#32346" +msgid "By Name" +msgstr "Podle názvu" + +msgctxt "#32347" +msgid "artists" +msgstr "interpreti" + +msgctxt "#32348" +msgid "movies" +msgstr "filmy" + +msgctxt "#32349" +msgid "photos" +msgstr "fotky" + +msgctxt "#32350" +msgid "shows" +msgstr "seriály" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Podle data přidání" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Datum přidání" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Podle data vydání" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Datum vydání" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Podle data shlédnutí" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Datum shlédnutí" + +msgctxt "#32357" +msgid "By Name" +msgstr "Podle názvu" + +msgctxt "#32358" +msgid "Name" +msgstr "Název" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Podle hodnocení" + +msgctxt "#32360" +msgid "Rating" +msgstr "Hodnocení" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Podle rozlišení" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Rozlišení" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Podle délky" + +msgctxt "#32364" +msgid "Duration" +msgstr "Délka" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Podle prvního vysílání" + +msgctxt "#32366" +msgid "First Aired" +msgstr "První vysílání" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Podle neshlédnutí" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Neshlédnuté" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Podle data přehrání" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Datum přehrání" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Podle počtu přehrání" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Počet přehrání" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Podle data pořízení" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Datum pořízení" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Filtry nejsou k dispozici" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Odstranit filtr" + +msgctxt "#32377" +msgid "Year" +msgstr "Rok" + +msgctxt "#32378" +msgid "Decade" +msgstr "Desetiletí" + +msgctxt "#32379" +msgid "Genre" +msgstr "Žánr" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Hodnocení obsahu" + +msgctxt "#32381" +msgid "Network" +msgstr "Stanice" + +msgctxt "#32382" +msgid "Collection" +msgstr "Kolekce" + +msgctxt "#32383" +msgid "Director" +msgstr "Režie" + +msgctxt "#32384" +msgid "Actor" +msgstr "Herci" + +msgctxt "#32385" +msgid "Country" +msgstr "Země" + +msgctxt "#32386" +msgid "Studio" +msgstr "Studio" + +msgctxt "#32387" +msgid "Labels" +msgstr "Popisky" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Výrobce kamery" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Model kamery" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Clona" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Rychlost závěrky" + +msgctxt "#32392" +msgid "Lens" +msgstr "Čočka" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Seriály" + +msgctxt "#32394" +msgid "Music" +msgstr "Hudba" + +msgctxt "#32395" +msgid "Audio" +msgstr "Zvuková stopa" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Titulky" + +msgctxt "#32397" +msgid "Quality" +msgstr "Kvalita" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Video nastavení Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Audio nastavení Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Přejít na sezónu" + +msgctxt "#32401" +msgid "Directors" +msgstr "Režiséři" + +msgctxt "#32402" +msgid "Writer" +msgstr "Autor" + +msgctxt "#32403" +msgid "Writers" +msgstr "Autoři" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Podobné filmy" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Stáhnout titulky" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Zpoždění titulků" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Další titulky" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Vypnout titulky" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Zapnout titulky" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Verze platformy" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Neznámá" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Upravit nebo smazat" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Upravit IP adresu nebo smazat nastavení?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Smazat" + +msgctxt "#32415" +msgid "Edit" +msgstr "Upravit" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Zadejte IP adresu" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Zadejte číslo portu" + +msgctxt "#32418" +msgid "Creator" +msgstr "Tvůrce" + +msgctxt "#32419" +msgid "Cast" +msgstr "Hrají" + +msgctxt "#32420" +msgid "Disc" +msgstr "Disk" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Odhlásit" + +msgctxt "#32422" +msgid "Exit" +msgstr "Odejít" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Vypnout" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Uspat" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Hibernovat" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Restartovat" + +msgctxt "#32427" +msgid "Failed" +msgstr "Neuspělo" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Přihlášení neuspělo!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Pokračovat od {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Objevit" + +msgctxt "#32431" +msgid "Search" +msgstr "Vyhledat" + +msgctxt "#32432" +msgid "Space" +msgstr "Mezera" + +msgctxt "#32433" +msgid "Clear" +msgstr "Smazat" + +msgctxt "#32434" +msgid "Searching..." +msgstr "Vyhledávám..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Žádné výsledky" + +msgctxt "#32436" +msgid "Paused" +msgstr "Pozastaveno" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Vítáme vás" + +msgctxt "#32438" +msgid "Previous" +msgstr "Předchozí" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Přehrávám další" + +msgctxt "#32440" +msgid "On Deck" +msgstr "Aktuální" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Neznámý" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Vestavěné" + +msgctxt "#32443" +msgid "Forced" +msgstr "Vynucené" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Text skladby" + +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Stereo" + +msgctxt "#32447" +msgid "None" +msgstr "Žádné" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Přehrávání neuspělo!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Nemohu se připojit k plex.tv[CR]Zkontrolujte připojení k internetu a zkuste to znovu." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Zvolte verzi" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Přehrávám verzi..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "V této knihovně není žádný obsah" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Prosím přidejte obsah a nebo zkontrolujte, že je povolené nastavení 'přidat na nástěnku'." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Pro tento filtr není k dispozici žádný obsah" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Prosím změňte nebo odstraňte filtr" + +msgctxt "#32456" +msgid "Offline Mode" +msgstr "Režim offline" + +msgctxt "#32457" +msgid "Sign In" +msgstr "Přihlásit" diff --git a/script.plexmod/resources/language/resource.language.de_de/strings.po b/script.plexmod/resources/language/resource.language.de_de/strings.po new file mode 100644 index 000000000..b50c731b5 --- /dev/null +++ b/script.plexmod/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,1946 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: PM4K\n" +"Language: de\n" + +#: +msgctxt "#32000" +msgid "Main" +msgstr "Allgemein" + +#: +msgctxt "#32001" +msgid "Original" +msgstr "Original" + +#: +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mbit/s 1080p" + +#: +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mbit/s 1080p" + +#: +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mbit/s 1080p" + +#: +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mbit/s 1080p" + +#: +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mbit/s 720p" + +#: +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mbit/s 720p" + +#: +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mbit/s 720p" + +#: +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mbit/s 480p" + +#: +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kbit/s" + +#: +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kbit/s" + +#: +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kbit/s" + +#: +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kbit/s" + +#: +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kbit/s" + +#: +msgctxt "#32020" +msgid "Local Quality" +msgstr "Qualität für lokale Geräte" + +#: +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Qualität für entfernte Geräte" + +#: +msgctxt "#32022" +msgid "Online Quality" +msgstr "Online Qualität" + +#: +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Transkodierungsformat" + +#: +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Debug-Protokollierung" + +#: +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Direkte Wiedergabe erlauben" + +#: +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Direktes streamen erlauben" + +#: +msgctxt "#32027" +msgid "Force" +msgstr "Erzwingen" + +#: +msgctxt "#32028" +msgid "Always" +msgstr "Immer" + +#: +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "nur Bildformate" + +#: +msgctxt "#32030" +msgid "Auto" +msgstr "Automatisch" + +#: +msgctxt "#32031" +msgid "Burn-in Subtitles" +msgstr "Untertitel einbrennen" + +#: +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Unsichere Verbindung erlauben" + +#: +msgctxt "#32033" +msgid "Never" +msgstr "Nie" + +#: +msgctxt "#32034" +msgid "On Same network" +msgstr "Im selben Netzwerk" + +#: +msgctxt "#32035" +msgid "Always" +msgstr "Immer" + +#: +msgctxt "#32036" +msgid "Allow 4K" +msgstr "4K erlauben" + +#: +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "HEVC erlauben (H265)" + +#: +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Automatisch anmelden" + +#: +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Automatisch nächsten Titel abspielen" + +#: +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Untertitel download aktivieren" + +#: +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Untertitel download aktivieren" + +#: +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Server Erkennung (GDM)" + +#: +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Plex mit Kodi starten" + +#: +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "Verbindung 1 IP" + +#: +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Verbindung 1 Port" + +#: +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "Verbindung 2 IP" + +#: +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Verbindung 2 Port" + +#: +msgctxt "#32048" +msgid "Audio" +msgstr "Ton" + +#: +msgctxt "#32049" +msgid "Advanced" +msgstr "Erweitert" + +#: +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Manuelle Server" + +#: +msgctxt "#32051" +msgid "Privacy" +msgstr "Datenschutz" + +#: +msgctxt "#32052" +msgid "About" +msgstr "Über" + +#: +msgctxt "#32053" +msgid "Video" +msgstr "Video" + +#: +msgctxt "#32054" +msgid "Addon Version" +msgstr "Erweiterung Version" + +#: +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Kodi Version" + +#: +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Bilschirmauflösung" + +#: +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Aktuelle Server Version" + +#: +msgctxt "#32058" +msgid "Never exceed original audio codec" +msgstr "Niemals den ursprünglichen Audio-Codec überschreiten" + +#: +msgctxt "#32059" +msgid "When transcoding audio, never exceed the original audio bitrate or channel count on the same codec." +msgstr "Bei der Transkodierung von Audio niemals die ursprüngliche Audio-Bitrate oder die Anzahl der Kanäle mit demselben Codec überschreiten." + +#: +msgctxt "#32060" +msgid "Use Kodi audio channels" +msgstr "Kodi-Audiokanäle verwenden" + +#: +msgctxt "#32061" +msgid "When transcoding audio, target the audio channels set in Kodi." +msgstr "Beim Transkodieren von Audio werden die in Kodi eingestellten Audiokanäle verwendet." + +#: +msgctxt "#32062" +msgid "Transcode audio to AC3" +msgstr "Transkodiere Ton zu AC3" + +#: +msgctxt "#32063" +msgid "Transcode audio to AC3 in certain conditions (useful for passthrough)." +msgstr "Transkodiere Ton zu AC3 in bestimmten Umständen (nützlich für Passthrough)" + +#: +msgctxt "#32064" +msgid "Treat DTS like AC3" +msgstr "DTS wie AC3 behandeln" + +#: +msgctxt "#32065" +msgid "When any of the force AC3 settings are enabled, treat DTS the same as AC3 (useful for Optical passthrough)" +msgstr "Wenn einer der AC3 Optionen aktiviert ist, wird DTS wie AC3 behandelt (nützlich für Optical Passthrough)" + + +msgctxt "#32066" +msgid "Force audio to AC3" +msgstr "Audio immer als AC3 erzwingen" + + +msgctxt "#32067" +msgid "Only force multichannel audio to AC3" +msgstr "Nur Mehrkanal-Audio als AC3 erzwingen" + +#: +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Benutzerauswahl und Pin-Eingabe beim Start überspringen." + +#: +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Falls aktiviert wird, sofern verfügbar, nachdem die Wiedergabe endet ein 'nächster Titel' automatisch nach 15 Sekunden gestartet." + +#: +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Aktiviert 4K Wiedergabe per Hardware. Deaktivierung erzwingt Transcodierung." + +#: +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Aktiviert HEVC/H265 Wiedergabe per Hardware. Deaktivierung erzwingt Transcodierung." + +#: +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Wann soll zu Servern ohne 'sichere Verbindung' verbunden werden[CR]* [B]Nie[/B]: Niemals unsicher zu eine Server verbinden.[CR]* [B]Im selben Netzwerk[/B]: Im selben Netzwerk erlauben[CR]* [B]Immer[/B]: Im selben Netzwerk und bei entfernter Verbindung erlauben" + +#: +msgctxt "#32201" +msgid "Trailer" +msgstr "Trailer" + +#: +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Entfernte Szene" + +#: +msgctxt "#32203" +msgid "Interview" +msgstr "Interview" + +#: +msgctxt "#32204" +msgid "Music Video" +msgstr "Musik-Video" + +#: +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Hinter den Kulissen" + +#: +msgctxt "#32206" +msgid "Scene" +msgstr "Szene" + +#: +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Live Musikvideo" + +#: +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Musikvideo mit Lyrics" + +#: +msgctxt "#32209" +msgid "Concert" +msgstr "Konzert" + +#: +msgctxt "#32210" +msgid "Featurette" +msgstr "Featurette" + +#: +msgctxt "#32211" +msgid "Short" +msgstr "Kurzfilm" + +#: +msgctxt "#32212" +msgid "Other" +msgstr "Sonstige" + +#: +msgctxt "#32300" +msgid "Go to Album" +msgstr "Gehe zu Album" + +#: +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Gehe zu Künstler(in)" + +#: +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Gehe zu {0}" + +#: +msgctxt "#32303" +msgid "Season" +msgstr "Staffel" + +#: +msgctxt "#32304" +msgid "Episode" +msgstr "Folge" + +#: +msgctxt "#32305" +msgid "Extras" +msgstr "Extras" + +#: +msgctxt "#32306" +msgid "Related Shows" +msgstr "Ähnliche Serien" + +#: +msgctxt "#32307" +msgid "More" +msgstr "Film" + +#: +msgctxt "#32308" +msgid "Available" +msgstr "Verfügbar" + +#: +msgctxt "#32309" +msgid "None" +msgstr "Ohne" + +#: +msgctxt "#32310" +msgid "S" +msgstr "S" + +#: +msgctxt "#32311" +msgid "E" +msgstr "E" + +#: +msgctxt "#32312" +msgid "Unavailable" +msgstr "Nicht verfügbar" + +#: +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Dieser Eintrag ist aktuell nicht verfügbar." + +#: +msgctxt "#32314" +msgid "In Progress" +msgstr "in Bearbeitung" + +#: +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Wiedergabe fortsetzen?" + +#: +msgctxt "#32316" +msgid "Resume" +msgstr "Fortsetzen" + +#: +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Von vorne wiedergeben" + +#: +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Als ungesehen markieren" + +#: +msgctxt "#32319" +msgid "Mark Played" +msgstr "Als gesehen markieren" + +#: +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Staffel als ungesehen markieren" + +#: +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Staffel als gesehen markieren" + +#: +msgctxt "#32322" +msgid "Delete" +msgstr "Löschen" + +#: +msgctxt "#32323" +msgid "Go To Show" +msgstr "Gehe zu Serie" + +#: +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Gehe zu {0}" + +#: +msgctxt "#32325" +msgid "Play Next" +msgstr "Nächster Titel" + +#: +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Wirklich löschen?" + +#: +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Soll diese Mediandatei wirklich gelöscht werden?" + +#: +msgctxt "#32328" +msgid "Yes" +msgstr "Ja" + +#: +msgctxt "#32329" +msgid "No" +msgstr "Nein" + +#: +msgctxt "#32330" +msgid "Message" +msgstr "Nachricht" + +#: +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Es gab einen Fehler beim Löschversuch dieser Mediendatei." + +#: +msgctxt "#32332" +msgid "Home" +msgstr "Start" + +#: +msgctxt "#32333" +msgid "Playlists" +msgstr "Wiedergabelisten" + +#: +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Beenden bestätigen" + +#: +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Bereit um Plex zu beenden?" + +#: +msgctxt "#32336" +msgid "Exit" +msgstr "Beenden" + +#: +msgctxt "#32337" +msgid "Cancel" +msgstr "Abbrechen" + +#: +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Keine Server gefunden" + +#: +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Server ist nicht erreichbar" + +#: +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Verbindungstests laufen. Bitte warten." + +#: +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Der Server ist nicht erreichbar. Bitte einloggen und Verbindung prüfen." + +#: +msgctxt "#32342" +msgid "Switch User" +msgstr "Benutzer wechseln" + +#: +msgctxt "#32343" +msgid "Settings" +msgstr "Einstellungen" + +#: +msgctxt "#32344" +msgid "Sign Out" +msgstr "Abmelden" + +#: +msgctxt "#32345" +msgid "All" +msgstr "Alle" + +#: +msgctxt "#32346" +msgid "By Name" +msgstr "Nach Name" + +#: +msgctxt "#32347" +msgid "Artists" +msgstr "Künstler" + +#: +msgctxt "#32348" +msgid "Movies" +msgstr "Filme" + +#: +msgctxt "#32349" +msgid "photos" +msgstr "Fotos" + +#: +msgctxt "#32350" +msgid "Shows" +msgstr "Serien" + +#: +msgctxt "#32351" +msgid "By Date Added" +msgstr "Nach 'Hinzugefügt am'" + +#: +msgctxt "#32352" +msgid "Date Added" +msgstr "Hinzugefügt am" + +#: +msgctxt "#32353" +msgid "By Release Date" +msgstr "Nach Erscheinungsdatum" + +#: +msgctxt "#32354" +msgid "Release Date" +msgstr "Erscheinungsdatum" + +#: +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Nach 'Gesehen am'" + +#: +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Gesehen am" + +#: +msgctxt "#32357" +msgid "By Name" +msgstr "Nach Name" + +#: +msgctxt "#32358" +msgid "Name" +msgstr "Name" + +#: +msgctxt "#32359" +msgid "By Rating" +msgstr "Nach Bewertung" + +#: +msgctxt "#32360" +msgid "Rating" +msgstr "Bewertung" + +#: +msgctxt "#32361" +msgid "By Resolution" +msgstr "Nach Auflösung" + +#: +msgctxt "#32362" +msgid "Resolution" +msgstr "Auflösung" + +#: +msgctxt "#32363" +msgid "By Duration" +msgstr "Nach Dauer" + +#: +msgctxt "#32364" +msgid "Duration" +msgstr "Dauer" + +#: +msgctxt "#32365" +msgid "By First Aired" +msgstr "Nach Erstausstrahlung" + +#: +msgctxt "#32366" +msgid "First Aired" +msgstr "Erstausstrahlung" + +#: +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Nach Ungesehen" + +#: +msgctxt "#32368" +msgid "Unplayed" +msgstr "Ungesehen" + +#: +msgctxt "#32369" +msgid "By Date Played" +msgstr "Nach 'Abgespielt am'" + +#: +msgctxt "#32370" +msgid "Date Played" +msgstr "Abgespielt am" + +#: +msgctxt "#32371" +msgid "By Play Count" +msgstr "Nach Wiedergabe-Zähler" + +#: +msgctxt "#32372" +msgid "Play Count" +msgstr "Wiedergabe-Zähler" + +#: +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Nach Aufnahmedatum" + +#: +msgctxt "#32374" +msgid "Date Taken" +msgstr "Aufnahmedatum" + +#: +msgctxt "#32375" +msgid "No filters available" +msgstr "Keine Filter verfügbar" + +#: +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Filter bereinigen" + +#: +msgctxt "#32377" +msgid "Year" +msgstr "Jahr" + +#: +msgctxt "#32378" +msgid "Decade" +msgstr "Jahrzent" + +#: +msgctxt "#32379" +msgid "Genre" +msgstr "Genre" + +#: +msgctxt "#32380" +msgid "Content Rating" +msgstr "Jugendfreigabe" + +#: +msgctxt "#32381" +msgid "Network" +msgstr "Netzwerk" + +#: +msgctxt "#32382" +msgid "Collection" +msgstr "Sammlung" + +#: +msgctxt "#32383" +msgid "Director" +msgstr "Regisseur(in)" + +#: +msgctxt "#32384" +msgid "Actor" +msgstr "Schauspieler(in)" + +#: +msgctxt "#32385" +msgid "Country" +msgstr "Land" + +#: +msgctxt "#32386" +msgid "Studio" +msgstr "Studio" + +#: +msgctxt "#32387" +msgid "Labels" +msgstr "Labels" + +#: +msgctxt "#32388" +msgid "Camera Make" +msgstr "Kamerahersteller" + +#: +msgctxt "#32389" +msgid "Camera Model" +msgstr "Kameramodell" + +#: +msgctxt "#32390" +msgid "Aperture" +msgstr "Blendenöffnung" + +#: +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Verschlusszeit" + +#: +msgctxt "#32392" +msgid "Lens" +msgstr "Linse" + +#: +msgctxt "#32393" +msgid "TV Shows" +msgstr "Fernsehserien" + +#: +msgctxt "#32394" +msgid "Music" +msgstr "Musik" + +#: +msgctxt "#32395" +msgid "Audio" +msgstr "Ton" + +#: +msgctxt "#32396" +msgid "Subtitles" +msgstr "Untertitel" + +#: +msgctxt "#32397" +msgid "Quality" +msgstr "Qualität" + +#: +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Kodi Bild Einstellungen" + +#: +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Kodi Ton Einstellungen" + +#: +msgctxt "#32400" +msgid "Go To Season" +msgstr "Gehe zu Staffel" + +#: +msgctxt "#32401" +msgid "Directors" +msgstr "Regisseure" + +#: +msgctxt "#32402" +msgid "Writer" +msgstr "Autor(in)" + +#: +msgctxt "#32403" +msgid "Writers" +msgstr "Autoren" + +#: +msgctxt "#32404" +msgid "Related Movies" +msgstr "Ähnliche Filme" + +#: +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Untertitel downloaden" + +#: +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Untertitel Verzögerung" + +#: +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Nächster Untertitel" + +#: +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Untertitel deaktiviren" + +#: +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Untertitel aktivieren" + +#: +msgctxt "#32410" +msgid "Platform Version" +msgstr "Platform Version" + +#: +msgctxt "#32411" +msgid "Unknown" +msgstr "Unbekannt" + +#: +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Bearbeiten oder bereinigen" + +#: +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "IP Adresse bearbeiten oder die aktuellen Einstellungen bereinigen?" + +#: +msgctxt "#32414" +msgid "Clear" +msgstr "Bereinigen" + +#: +msgctxt "#32415" +msgid "Edit" +msgstr "Ändern" + +#: +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "IP Adresse eingeben" + +#: +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Port Nummer eingeben" + +#: +msgctxt "#32418" +msgid "Creator" +msgstr "Urheber(in)" + +#: +msgctxt "#32419" +msgid "Cast" +msgstr "Besetzung" + +#: +msgctxt "#32420" +msgid "Disc" +msgstr "Disk" + +#: +msgctxt "#32421" +msgid "Sign Out" +msgstr "Abmelden" + +#: +msgctxt "#32422" +msgid "Exit" +msgstr "Beenden" + +#: +msgctxt "#32423" +msgid "Shutdown" +msgstr "Herunterfahren" + +#: +msgctxt "#32424" +msgid "Suspend" +msgstr "Energie sparen" + +#: +msgctxt "#32425" +msgid "Hibernate" +msgstr "Ruhezustand" + +#: +msgctxt "#32426" +msgid "Reboot" +msgstr "Neustart" + +#: +msgctxt "#32427" +msgid "Failed" +msgstr "Fehlgeschlagen" + +#: +msgctxt "#32428" +msgid "Login failed!" +msgstr "Anmeldung fehlgeschlagen!" + +#: +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Fortsetzen ab {0}" + +#: +msgctxt "#32430" +msgid "Discovery" +msgstr "Entdecken" + +#: +msgctxt "#32431" +msgid "Search" +msgstr "Suche" + +#: +msgctxt "#32432" +msgid "Space" +msgstr "Leerzeichen" + +#: +msgctxt "#32433" +msgid "Clear" +msgstr "Bereinigen" + +#: +msgctxt "#32434" +msgid "Searching..." +msgstr "Suche..." + +#: +msgctxt "#32435" +msgid "No Results" +msgstr "Keine Ergebnisse" + +#: +msgctxt "#32436" +msgid "Paused" +msgstr "Pausiert" + +#: +msgctxt "#32437" +msgid "Welcome" +msgstr "Willkommen" + +#: +msgctxt "#32438" +msgid "Previous" +msgstr "Vorheriger Titel" + +#: +msgctxt "#32439" +msgid "Playing Next" +msgstr "Nächster Titel" + +#: +msgctxt "#32440" +msgid "On Deck" +msgstr "Aktuell" + +#: +msgctxt "#32441" +msgid "Unknown" +msgstr "Unbekannt" + +#: +msgctxt "#32442" +msgid "Embedded" +msgstr "Eingebettet" + +#: +msgctxt "#32443" +msgid "Forced" +msgstr "Zwangsuntertitel" + +#: +msgctxt "#32444" +msgid "Lyrics" +msgstr "Lyrics" + +#: +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +#: +msgctxt "#32446" +msgid "Stereo" +msgstr "Stereo" + +#: +msgctxt "#32447" +msgid "None" +msgstr "Nichts" + +#: +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Wiedergabe fehlgeschlagen!" + +#: +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Keine Verbindung zu plex.tv[CR]Internetverbindung prüfen und erneut versuchen." + +#: +msgctxt "#32450" +msgid "Choose Version" +msgstr "Version auswählen" + +#: +msgctxt "#32451" +msgid "Play Version..." +msgstr "Version abspielen..." + +#: +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "In dieser Bibliothek ist nichts vorhanden" + +#: +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Bitte Inhalte ergänzen und/oder prüfen ob 'In Dashboard anzeigen' aktiv ist." + +#: +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Mit diesem Filter ist kein Inhalt verfügbar" + +#: +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Bitte aktuellen Filter ändern oder entfernen" + +#: +msgctxt "#32456" +msgid "Show" +msgstr "Serie" + +#: +msgctxt "#32457" +msgid "By Show" +msgstr "Nach Serie" + +#: +msgctxt "#32458" +msgid "Episodes" +msgstr "Episoden" + +#: +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Offline-Modus" + +#: +msgctxt "#32460" +msgid "Sign In" +msgstr "Anmelden" + +#: +msgctxt "#32461" +msgid "Albums" +msgstr "Alben" + +#: +msgctxt "#32462" +msgid "Artist" +msgstr "Künstler(in)" + +#: +msgctxt "#32463" +msgid "By Artist" +msgstr "Nach Künstler(in)" + +#: +msgctxt "#32464" +msgid "Player" +msgstr "Wiedergabe" + +#: +msgctxt "#32465" +msgid "Use skip step settings from Kodi" +msgstr "Skip-Schritte-Einstellung von Kodi benutzen" + +#: +msgctxt "#32466" +msgid "Automatically seek selected position after a delay" +msgstr "Automatisch verzögert zur ausgewählten Position springen" + +#: +msgctxt "#32467" +msgid "User Interface" +msgstr "Benutzeroberfläche" + +#: +msgctxt "#32468" +msgid "Show dynamic background art" +msgstr "Dynamische Hintergrundbilder aktivieren" + +#: +msgctxt "#32469" +msgid "Background art blur amount" +msgstr "Unschärfe der Hintergrundbilder" + +#: +msgctxt "#32470" +msgid "Background art opacity" +msgstr "Deckkraft der Hintergrundbilder" + +#: +msgctxt "#32471" +msgid "Use Plex/Kodi steps for timeline" +msgstr "Plex/Kodi-Skip-Schritte für die Zeitachse verwenden" + +#: +msgctxt "#32480" +msgid "Theme music" +msgstr "Titelmusik" + +#: +msgctxt "#32481" +msgid "Off" +msgstr "Aus" + +#: +msgctxt "#32482" +msgid "%(percentage)s %%" +msgstr "%(percentage)s %%" + +#: +msgctxt "#32483" +msgid "Hide Stream Info" +msgstr "Stream Info nicht anzeigen" + +#: +msgctxt "#32484" +msgid "Show Stream Info" +msgstr "Stream Info anzeigen" + +#: +msgctxt "#32485" +msgid "Go back instantly with the previous menu action in scrolled views" +msgstr "Mit der Previous-Menu-Aktion In gescrollten Ansichten sofort zurückgehen" + +#: +msgctxt "#32487" +msgid "Seek Delay" +msgstr "Vorspulverzögerung" + +#: +msgctxt "#32488" +msgid "Screensaver" +msgstr "Bildschirmschoner" + +#: +msgctxt "#32489" +msgid "Quiz Mode" +msgstr "Quiz-Modus" + +#: +msgctxt "#32490" +msgid "Collections" +msgstr "Sammlungen" + +#: +msgctxt "#32491" +msgid "Folders" +msgstr "Ordner" + +#: +msgctxt "#32492" +msgid "Kodi Subtitle Settings" +msgstr "Kodi Untertitel-Einstellungen" + +#: +msgctxt "#32493" +msgid "When a media file has a forced/foreign subtitle for a subtitle-enabled language, the Plex Media Server preselects it. This behaviour is usually not necessary and not configurable. This setting fixes that by ignoring the PMSs decision and selecting the same language without a forced flag if possible." +msgstr "Hat eine Mediendatei erzwungenge Untertitel für eine Sprache, bei der Untertitel erwünscht sind, wählt der Plex Media Server diesen standardmäßig aus. Das Verhalten ist normalerweise unerwünscht und nicht konfigurierbar. Diese Einstellung behebt das Problem, indem versucht wird, einen Untertitel der selben Sprache ohne Erzwungen-Markierung zu wählen." + +#: +msgctxt "#32495" +msgid "Skip intro" +msgstr "Intro überspringen" + +#: +msgctxt "#32496" +msgid "Skip credits" +msgstr "Abspann überspringen" + +#: +msgctxt "#32500" +msgid "Always show post-play screen (even for short videos)" +msgstr "Immer den Nachwiedergabe-Bildschirm anzeigen (auch bei kurzen Videos)" + +#: +msgctxt "#32501" +msgid "Time-to-wait between videos on post-play" +msgstr "Wartezeit zwischen Videos bei Nachwiedergabe" + +#: +msgctxt "#32505" +msgid "Visit media in video playlist instead of playing it" +msgstr "Medien in der Video-Wiedergabeliste anzeigen, anstatt sie abzuspielen" + +#: +msgctxt "#32521" +msgid "Skip Intro Button Timeout" +msgstr "Zeitlimit für die Schaltfläche Intro überspringen" + +#: +msgctxt "#32522" +msgid "Automatically Skip Intro" +msgstr "Intro automatisch überspringen" + +#: +msgctxt "#32523" +msgid "Automatically skip intros if available." +msgstr "Automatisches Überspringen von Intros, falls vorhanden. Überschreibt aktivierten Binge-Modus nicht." + +#: +msgctxt "#32524" +msgid "Set how long the skip intro button shows for." +msgstr "Festlegen, wie lange die Schaltfläche zum Überspringen des Intros angezeigt werden soll." + +#: +msgctxt "#32525" +msgid "Skip Credits Button Timeout" +msgstr "Zeitlimit für die Schaltfläche Abspann überspringen" + +#: +msgctxt "#32526" +msgid "Automatically Skip Credits" +msgstr "Automatisches Überspringen von Abspann" + +#: +msgctxt "#32527" +msgid "Automatically skip credits if available." +msgstr "Automatisches Überspringen von Abspann, falls vorhanden. Überschreibt aktivierten Binge-Modus nicht." + +#: +msgctxt "#32528" +msgid "Set how long the skip credits button shows for." +msgstr "Festlegen, wie lange die Schaltfläche zum Überspringen des Abspann angezeigt werden soll." + +#: +msgctxt "#32540" +msgid "Show when the current video will end in player" +msgstr "Im Player anzeigen, wann das aktuelle Video endet" + +#: +msgctxt "#32541" +msgid "Shows time left and at which time the media will end." +msgstr "Zeigt im Player an, wann und zu welchem Zeitpunkt das aktuelle Video endet." + +#: +msgctxt "#32542" +msgid "Show \"Ends at\" label for the end-time as well" +msgstr "\"Endet um\"-Label für die Endzeit auch anzeigen" + +#: +msgctxt "#32543" +msgid "Ends at" +msgstr "Endet um" + +#: +msgctxt "#32601" +msgid "Allow AV1" +msgstr "AV1 zulassen" + +#: +msgctxt "#32602" +msgid "Enable this if your hardware can handle AV1. Disable it to force transcoding." +msgstr "Diese Option aktivieren, wenn die Hardware AV1 verarbeiten kann. Deaktivieren, um die Transkodierung zu erzwingen." + +#: +msgctxt "#33101" +msgid "By Audience Rating" +msgstr "Nach Zuschauerwertung" + +#: +msgctxt "#33102" +msgid "Audience Rating" +msgstr "Zuschauerwertung" + +#: +msgctxt "#33103" +msgid "By my Rating" +msgstr "Nach meiner Bewertung" + +#: +msgctxt "#33104" +msgid "My Rating" +msgstr "Meine Bewertung" + +#: +msgctxt "#33105" +msgid "By Content Rating" +msgstr "Nach Inhaltsbewertung" + +#: +msgctxt "#33106" +msgid "Content Rating" +msgstr "Inhaltsbewertung" + +#: +msgctxt "#33107" +msgid "By Critic Rating" +msgstr "Nach kritischer Bewertung" + +#: +msgctxt "#33108" +msgid "Critic Rating" +msgstr "Kritikerwertung" + +#: +msgctxt "#33200" +msgid "Background Color" +msgstr "Hintergrundfarbe" + +#: +msgctxt "#33201" +msgid "Specify solid Background Color instead of using media images" +msgstr "Hintergrundfarbe anstatt Bilder verwenden" + +#: +msgctxt "#33400" +msgid "Use old compatibility profile" +msgstr "Altes Kompatibilitätsprofil verwenden" + +#: +msgctxt "#33401" +msgid "Uses the Chrome client profile instead of the custom one. Might fix rare issues with 3D playback." +msgstr "Benutzt das alte Chrome Client-Profil anstatt des angepassten. Könnte seltene Fehler beim Abspielen von 3D-Inhalten verhindern." + +#: +msgctxt "#32348" +msgid "movies" +msgstr "Filme" + +#: +msgctxt "#32466" +msgid "Automatically seek to the selected timeline position after a second" +msgstr "Nach Verzögerung automatisch zur aktuell gewählten Position springen" + +msgctxt "#33501" +msgid "Video played threshold" +msgstr "Video-abgespielt-Grenzwert" + +msgctxt "#33502" +msgid "Set this to the same value as your Plex server (Settings>Library>Video played threshold) to avoid certain pitfalls, Default: 90 %" +msgstr "Auf dem selben wert wie im Plex server setzen (Einstellungen>Mediathek>Video played threshold) um bestimmte Fehler zu umgehen, Standardwert: 90 %" + +msgctxt "#33503" +msgid "Use alternative hubs refresh" +msgstr "Alternative Aktualisierung der Hubs" + +msgctxt "#33504" +msgid "Refreshes all hubs for all libraries after an item's watch-state has changed, instead of only those likely affected. Use this if you find a hub that doesn't update properly." +msgstr "Aktualisiert alle Hubs aller Bibliotheken, anstatt nur die, die möglicherweise zutreffend sind, nachdem sich der Abspielstatus eines Items geändert hat. Benutzen, wenn sich ein Hub nicht aktualisiert." + +msgctxt "#33505" +msgid "Show intro skip button early" +msgstr "Intro überspringen früher anzeigen" + +msgctxt "#33506" +msgid "Show the intro skip button from the start of a video with an intro marker. The auto-skipping setting applies." +msgstr "Zeige den Intro-Überspringen-Knopf von Anfang an. Die Automatische-Intro-Überspringen-Einstellung wird angewandt. Überschreibt aktivierten Binge-Modus nicht." + +msgctxt "#33507" +msgid "Enabled" +msgstr "Aktiviert" + +msgctxt "#33508" +msgid "Disabled" +msgstr "Deaktiviert" + +msgctxt "#33509" +msgid "Early intro skip threshold (default: < 60s/1m)" +msgstr "Frühes Intro-Überspringen Grenzwert (default: < 60s/1m)" + +msgctxt "#33510" +msgid "When showing the intro skip button early, only do so if the intro starts within the first X seconds." +msgstr "Wenn der Into-Überspringen-Knopf früher angezeigt werden soll, nur anzeigen, wenn das Intro innerhalb der ersten X Sekunden startet." + +msgctxt "#33600" +msgid "System" +msgstr "System" + +msgctxt "#33601" +msgid "Show video chapters" +msgstr "Video-Kapitel anzeigen" + +msgctxt "#33602" +msgid "If available, show video chapters from the video-file instead of the timeline-big-seek-steps." +msgstr "Wenn verfügbar, Video-Kapitel aus der Video-Datei anstatt der Zeitleiste-Big-Seek-Schritte anzeigen." + +msgctxt "#33603" +msgid "Use virtual chapters" +msgstr "Virtuelle Kapitel verwenden" + +msgctxt "#33604" +msgid "When the above is enabled and no video chapters are available, simulate them by using the markers identified by the Plex Server (Intro, Credits)." +msgstr "Wenn die obrige aktiviert ist und keine Video-Kapitel verfügbar sind, virtuelle Kapitel aus den Plex Server Markern (Intro, Abspann) erzeugen." + +msgctxt "#33605" +msgid "Video Chapters" +msgstr "Video-Kapitel" + +msgctxt "#33606" +msgid "Virtual Chapters" +msgstr "Virtuelle Kapitel" + +msgctxt "#33607" +msgid "Chapter {}" +msgstr "Kapitel {}" + +msgctxt "#33608" +msgid "Intro" +msgstr "Intro" + +msgctxt "#33609" +msgid "Credits" +msgstr "Abspann" + +msgctxt "#33610" +msgid "Main" +msgstr "Haupt" + +msgctxt "#33611" +msgid "Chapters" +msgstr "Kapitel" + +msgctxt "#33612" +msgid "Markers" +msgstr "Markierungen" + +msgctxt "#33613" +msgid "Kodi Buffer Size (MB)" +msgstr "Kodi Puffergröße (MB)" + +msgctxt "#33614" +msgid "Set the Kodi Cache/Buffer size. Free: {} MB, Recommended: ~100 MB, Recommended max: {} MB, Default: 20 MB." +msgstr "Setzt die Kodi Cache/Puffer Größe. Frei: {} MB, empfohlen: ~100 MB, empfohlenes Max.: {} MB, Default: 20 MB." + +msgctxt "#33615" +msgid "{time} left" +msgstr "{time} übrig" + +msgctxt "#33616" +msgid "Addon Path" +msgstr "Addon-Pfad" + +msgctxt "#33617" +msgid "Userdata/Profile Path" +msgstr "Benutzerdaten-/Profilpfad" + +msgctxt "#33618" +msgid "TV binge-viewing mode" +msgstr "TV Binge-Viewing-Modus" + +msgctxt "#33619" +msgid "Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t skip the intro of the first episode of a season.\n\nCan be disabled/enabled per TV show." +msgstr "Überspringt automatisch Intros und Abspänne von Episoden und versucht Recaps zu vermeiden. Überspringt das Intro der ersten Episode einer Staffel nicht." + +msgctxt "#33620" +msgid "Plex requests timeout (seconds)" +msgstr "Plex HTTP Zeitlimit (Sekunden)" + +msgctxt "#33621" +msgid "Set the (async and connection) timeout value of the Python requests library in seconds. Default: 5 seconds" +msgstr "Setzt das Zeitlimit für die Python Requests Bibliothek bei asynchronen Verbindungen und Verbindungsanfragen. Default: 5 Sekunden" + +msgctxt "#33622" +msgid "LAN reachability timeout (ms)" +msgstr "LAN-Erreichbarkeits-Zeitlimit (ms)" + +msgctxt "#33623" +msgid "When checking for Server-in-LAN reachability, use this timeout. Default: 10ms" +msgstr "Wenn die lokale Serververbindung im LAN überprüft wird, benutze dieses Zeitlimit. Default: 10ms" + +msgctxt "#33624" +msgid "Network" +msgstr "Netzwerk" + +msgctxt "#33625" +msgid "Smart LAN/local server discovery" +msgstr "Smartes LAN/lokale Server-Auffinden" + +msgctxt "#33626" +msgid "Checks whether servers returned from Plex.tv are actually local/in your LAN. For specific setups (e.g. Docker) Plex.tv might not properly detect a local server.\n\nNOTE: Only works on Kodi 19 or above." +msgstr "Überprüft, ob von Plex.tv zurückgegebene Server tatsächlich lokal/in Deinem LAN sind. Für bestimmte Setups (z. B. Docker) könnte Plex.tv einen lokalen Servern nicht als solchen entdecken.\n\nACHTUNG: Funktioniert nur unter Kodi 19 oder neuer." + +msgctxt "#33627" +msgid "Prefer LAN/local servers over security" +msgstr "LAN/lokale Server bevorzugen" + +msgctxt "#33628" +msgid "Prioritizes local connections over secure ones. Needs the proper setting in \"Allow Insecure Connections\" and the Plex Server's \"Secure connections\" at \"Preferred\". Can be used to enforce manual servers." +msgstr "Priorisiert lokale Verbindungen über sichere. Benötigt die korrekte Einstellung in \"Unsichere Verbindung erlauben\" und die Plex Server Einstellung \"Sichere Verbindungen\" auf \"Bevorzugt\". Kann verwendet werden um manuelle Server zu forcieren." + +msgctxt "#33629" +msgid "Auto-skip intro/credits offset" +msgstr "Auto-Überspringen-Versatz" + +msgctxt "#33630" +msgid "Intro/credits markers might be a little early in Plex. When auto skipping add (or subtract) this many seconds from the marker. This avoids cutting off content, while possibly skipping the marker a little late." +msgstr "Intro-/Abspann-Markierungen können ein wenig früh erscheinen in Plex. Wenn diese automatisch übersprungen werden sollen, addiere (oder subtrahiere) diese Menge an Sekunden. Das verhindert das verfrühte Abschneiden von Inhalten, kann aber zu etwas verspätetem Springen führen." + +msgctxt "#32631" +msgid "Playback (user-specific)" +msgstr "Wiedergabe (benutzerspezifisch)" + +msgctxt "#33632" +msgid "Server connectivity check timeout (seconds)" +msgstr "Server Konnektivitätscheck-Timeout (Sekunden)" + +msgctxt "#33633" +msgid "Set the maximum amount of time a server connection has to answer a connectivity request. Default: 2.5" +msgstr "Setzt die maximale Zeit, in der eine Serververbindung antworten muss. Voreinstellung: 2.5" + +msgctxt "#33634" +msgid "Combined Chapters" +msgstr "Kombinierte Kapitel" + +msgctxt "#33635" +msgid "Final Credits" +msgstr "Finaler Abspann" + +msgctxt "#32700" +msgid "Action on Sleep event" +msgstr "Aktion bei Sleep-Ereignis" + +msgctxt "#32701" +msgid "When Kodi receives a sleep event from the system, run the following action." +msgstr "Wenn Kodi ein Sleep-Ereignis vom System erhält, folgende Aktion ausführen." + +msgctxt "#32702" +msgid "Nothing" +msgstr "Nichts" + +msgctxt "#32703" +msgid "Stop playback" +msgstr "Abspielen stoppen" + +msgctxt "#32704" +msgid "Quit Kodi" +msgstr "Kodi beenden" + +msgctxt "#32705" +msgid "CEC Standby" +msgstr "CEC Standby" + +msgctxt "#32800" +msgid "Skipping intro" +msgstr "Überspringe Intro" + +msgctxt "#32801" +msgid "Skipping credits" +msgstr "Überspringe Abspann" + +msgctxt "#32900" +msgid "While playing back an item and seeking on the seekbar, automatically seek to the selected position after a delay instead of having to confirm the selection." +msgstr "Wird während des Abspielens die Position auf der Zeitleiste verändert, automatisch nach einer Verzögerung auf die gewählte Position springen, ohne diese bestätigen zu müssen." + +msgctxt "#32901" +msgid "Seek delay in seconds." +msgstr "Sprungverzögerung in Sekunden." + +msgctxt "#32902" +msgid "Kodi has its own skip step settings. Try to use them if they're configured instead of the default ones." +msgstr "Kodi besitzt seine eigenen Sprungeinstellungen. Versuche diese anstatt der standardmäßigen zu verwenden, sollten sie konfiguriert sein." + +msgctxt "#32903" +msgid "Use the above for seeking on the timeline as well." +msgstr "Benutze die obrige Einstellung auch für die Zeitleiste." + +msgctxt "#32904" +msgid "In seconds." +msgstr "In Sekunden." + +msgctxt "#32905" +msgid "Cancel post-play timer by pressing OK/SELECT" +msgstr "Post-Play Timer durch OK/AUSWAHL abbrechen" + +msgctxt "#32906" +msgid "Cancel skip marker timer with BACK" +msgstr "Markierung überspringen durch Zurück-Taste abbrechen" + +msgctxt "#32907" +msgid "When auto-skipping a marker, allow cancelling the timer by pressing BACK." +msgstr "Wenn eine Markierung mit einem Zeitgeber automatisch übersprungen wird, durch Zurück-Taste abbrechen." + +msgctxt "#32908" +msgid "Immediately skip marker with OK/SELECT" +msgstr "Markierung sofort überspringen mit OK/AUSWAHL" + +msgctxt "#32909" +msgid "When auto-skipping a marker with a timer, allow skipping immediately by pressing OK/SELECT." +msgstr "Wenn eine Markierung mit einem Zeitgeber automatisch übersprungen wird, durch OK/AUSWAHL-Taste sofort überspringen." + +msgctxt "#32912" +msgid "Show buffer-state on timeline" +msgstr "Zeige Puffer-Stand auf Zeitachse" + +msgctxt "#32913" +msgid "Shows the current Kodi buffer/cache state on the video player timeline." +msgstr "Zeigt den aktuellen Kodi Puffer/Cache-Stand auf der Videoplayer Zeitachse." + +msgctxt "#32914" +msgid "Loading" +msgstr "Lädt" + +msgctxt "#32915" +msgid "Slow connection" +msgstr "Langsame Verbindung" + +msgctxt "#32916" +msgid "Use with a wonky/slow connection, e.g. in a hotel room. Adjusts the UI to visually wait for item refreshes and waits for the buffer to fill when starting playback. Automatically sets readfactor=20, requires Kodi restart." +msgstr "Bei langsamer Verbindung benutzen, z.B. im Hotel. Passt die Oberfläche visuell an um auf Media Auffrischungen zu warten und füllt den Kodi Puffer vor dem Abspielen. Setzt automatisch Kodi Cache readfactor=20, benötigt einen Kodi Neustart." + +msgctxt "#32917" +msgid "Couldn't fill buffer in time ({}s)" +msgstr "Konnte den Puffer nicht schnell genug füllen ({} Sek.)" + +msgctxt "#32918" +msgid "Buffer wait timeout (seconds)" +msgstr "Puffer-Wartezeit (Sekunden)" + +msgctxt "#32919" +msgid "When slow connection is enabled in the addon, wait this long for the buffer to fill. Default: 120 s" +msgstr "Wenn Langsame Verbindung im Addon aktiviert ist, warte so lange bis der Puffer gefüllt ist. Voreinstellung: 120 Sek." + +msgctxt "#32920" +msgid "Insufficient buffer wait (seconds)" +msgstr "Ungenügender Puffer Wartezeit (Sekunden)" + +msgctxt "#32921" +msgid "When slow connection is enabled in the addon and the configured cache/buffer isn't big enough for us to determine its fill state, wait this long when starting playback. Default: 10 s" +msgstr "Wenn \"Langsame Verbindung\" im Addon aktiviert ist und der konfigurierte Puffer/Cache nicht groß genug ist, um seinen Füllstand zu ermitteln, wird so lange gewartet, bevor die Wiedergabe gestartet wird. Voreinstellung: 10 Sek." + +msgctxt "#32922" +msgid "Kodi Cache Readfactor" +msgstr "Kodi Puffer Readfactor" + +msgctxt "#32923" +msgid "Sets the Kodi cache readfactor value. Default: {0}, recommended: {1}. With \"Slow connection\" enabled this will be set to {2}, as otherwise the cache doesn't fill fast/aggressively enough." +msgstr "Setzt den Kodi Cache/Puffer readfactor Wert. Standard: {0}, Empfohlen: {1}. Bei aktivem \"Langsame Verbindung\" wird dies automatisch auf {2} gesetzt, da ansonsten der Cache nicht schnell/agressiv genug gefüllt wird." + +msgctxt "#32924" +msgid "Minimize" +msgstr "Minimieren" + +msgctxt "#32925" +msgid "Playback Settings" +msgstr "Abspieleinstell." + +msgctxt "#32926" +msgid "Wrong pin entered!" +msgstr "Falsche PIN eingegeben!" + +msgctxt "#32927" +msgid "Use episode thumbnails in continue hub" +msgstr "Vorschaubild für Episoden im Fortsetzen-Hub verwenden" + +msgctxt "#32928" +msgid "Instead of using media artwork, use thumbnails for episodes in the continue hub on the home screen if available." +msgstr "Verwende Vorschaubilder anstatt Media-Artwork für Episoden im Fortsetzen-Hub auf der Start-Ansicht." + +msgctxt "#32929" +msgid "Use legacy background fallback image" +msgstr "Veraltetes Ausweich-Hintergrundbild verwenden" + +msgctxt "#32930" +msgid "Previous Subtitle" +msgstr "Vorheriger Untertitel" + +msgctxt "#32931" +msgid "Audio/Subtitles" +msgstr "Audio/Untertitel" + +msgctxt "#32932" +msgid "Show subtitle quick-actions button" +msgstr "Zeige Untertitel-Schnellaktionen-Knopf" + +msgctxt "#32933" +msgid "Show FFWD/RWD buttons" +msgstr "Zeige Vorspulen-Zurückspulen-Knopf" + +msgctxt "#32934" +msgid "Show repeat button" +msgstr "Zeige Wiederholen-Knopf" + +msgctxt "#32935" +msgid "Show shuffle button" +msgstr "Zeige Zufällige-Wiedergabe-Knopf" + +msgctxt "#32936" +msgid "Show playlist button" +msgstr "Zeige Wiedergabeliste-Knopf" + +msgctxt "#32937" +msgid "Show prev/next button" +msgstr "Zeige Vorheriger/Nächster-Knopf" + +msgctxt "#32938" +msgid "Only for Episodes" +msgstr "Nur bei Episoden" + +msgctxt "#32939" +msgid "Only applies to video player UI" +msgstr "Gilt nur für die Video Abspieloberfläche" + +msgctxt "#32940" +msgid "Player UI" +msgstr "Abspieloberfläche" + +msgctxt "#32941" +msgid "Forced subtitles fix" +msgstr "Erzwungene Untertitel beheben" + +msgctxt "#32942" +msgid "Other seasons" +msgstr "Weitere Staffeln" + +msgctxt "#32943" +msgid "Crossfade dynamic background art" +msgstr "Dynamische Hintergrundbilder überblenden" + +msgctxt "#32944" +msgid "Burn-in SSA subtitles (DirectStream)" +msgstr "SSA-Untertitel einbrennen (DirectStream)" + +msgctxt "#32945" +msgid "When Direct Streaming instruct the Plex Server to burn in SSA/ASS subtitles (thus transcoding the video stream). If disabled it will not touch the video stream, but will convert the subtitle to unstyled text." +msgstr "Wird Direct Streaming verwendet, den Plex Server dazu bringen, SSA/ASS-Untertitel einzubrennen (also den Video Stream zu transcoden). Wenn deaktiviert, wird dieser den Video Stream nicht anfassen, jedoch den Untertitel als reinen Text anzeigen." + +msgctxt "#32946" +msgid "Stop video playback on idle after" +msgstr "Bei Inaktivität Video stoppen nach" + +msgctxt "#32947" +msgid "Stop video playback on screensaver" +msgstr "Video stoppen bei Bildschirmschoner" + +msgctxt "#32948" +msgid "Allow auto-skip when transcoding" +msgstr "Überspringen beim Transkodieren erlauben" + +msgctxt "#32949" +msgid "When transcoding/DirectStreaming, allow auto-skip functionality." +msgstr "Beim Transkodieren/DirectStream die automatische Überspringen-Funktionalität erlauben." + +msgctxt "#32950" +msgid "Use extended title for subtitles" +msgstr "Erweiterten Titel für Untertitel verwenden" + +msgctxt "#32951" +msgid "When displaying subtitles use the extendedDisplayTitle Plex exposes." +msgstr "Verwende erweiterte Namen wenn Untertitelnamen angezeigt werden." + +msgctxt "#32952" +msgid "Dialog-Flackern beheben" +msgstr "" + +msgctxt "#32953" +msgid "Reviews" +msgstr "Kritik" + +msgctxt "#32954" +msgid "Needs Kodi restart. WARNING: This will overwrite advancedsettings.xml!\n\nTo customize other cache/network-related values, copy \"script.plexmod/pm4k_cache_template.xml\" to profile folder and edit it to your liking. (See About section for the file paths)" +msgstr "Benötigt Kodi Neustart. ACHTUNG: Überschreibt advancedsettings.xml!\n\nUm weitere Cache-/Netzwerkbezogene Werte zu ändern, kopiere \"script.plexmod/pm4k_cache_template.xml\" in den Profilordner und editiere sie. (Für die Dateipfade schaue in die Über-Sektion)" + +msgctxt "#32955" +msgid "Use Kodi keyboard for searching" +msgstr "Kodi-Tastatur bei der Suche verwenden" + +msgctxt "#32956" +msgid "Poster resolution scaling %" +msgstr "Poster Auflösungsskalierung %" + +msgctxt "#32957" +msgid "In percent. Scales the resolution of all posters/thumbnails for better image quality. May impact PMS/PM4K performance, will increase the cache usage accordingly. Recommended: 200-300 % for big screens if your hardware can handle it. Needs addon restart." +msgstr "In Prozent. Skaliert die Auflösung aller Poster/Vorschaubilder für bessere Bildqualität. Kann die PMS/PM4K Performance beeinflussen, wird die Cache-Nutzung dementsprechend erhöhen. Empfohlen: 200-300 % für große Bildschirme, wenn die Hardware damit umgehen kann. Benötigt Addon-Neustart." + +msgctxt "#32958" +msgid "Calculate OpenSubtitles.com hash" +msgstr "OpenSubtitles.com Prüfsumme berechnen" + +msgctxt "#32959" +msgid "When opening the subtitle download feature, automatically calculate the OpenSubtitles.com hash for the given file. Can improve search results, downloads 2*64 KB of the video file to calculate the hash." +msgstr "Beim Öffnen der Untertitel-Herunterladen-Funktion die Prüfsumme der Datei für OpenSubtitles.com automatisch berechnen. Kann die Suchergebnisse verbessern, lädt 2*64 KB der Videodatei herunter um die Prüfsumme zu berechnen." diff --git a/script.plexmod/resources/language/resource.language.en_gb/strings.po b/script.plexmod/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 000000000..62f5ebd93 --- /dev/null +++ b/script.plexmod/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,1647 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "#32000" +msgid "Main" +msgstr "" + +msgctxt "#32001" +msgid "Original" +msgstr "" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "" + +msgctxt "#32027" +msgid "Force" +msgstr "" + +msgctxt "#32028" +msgid "Always" +msgstr "" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "" + +msgctxt "#32030" +msgid "Auto" +msgstr "" + +msgctxt "#32031" +msgid "Burn-in Subtitles" +msgstr "" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "" + +msgctxt "#32033" +msgid "Never" +msgstr "" + +msgctxt "#32034" +msgid "On Same network" +msgstr "" + +msgctxt "#32035" +msgid "Always" +msgstr "" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "" + +msgctxt "#32048" +msgid "Audio" +msgstr "" + +msgctxt "#32049" +msgid "Advanced" +msgstr "" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "" + +msgctxt "#32051" +msgid "Privacy" +msgstr "" + +msgctxt "#32052" +msgid "About" +msgstr "" + +msgctxt "#32053" +msgid "Video" +msgstr "" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "" + +msgctxt "#32058" +msgid "Never exceed original audio codec" +msgstr "" + +msgctxt "#32059" +msgid "When transcoding audio, never exceed the original audio bitrate or channel count on the same codec." +msgstr "" + +msgctxt "#32060" +msgid "Use Kodi audio channels" +msgstr "" + +msgctxt "#32061" +msgid "When transcoding audio, target the audio channels set in Kodi." +msgstr "" + +msgctxt "#32062" +msgid "Transcode audio to AC3" +msgstr "" + +msgctxt "#32063" +msgid "Transcode audio to AC3 in certain conditions (useful for passthrough)." +msgstr "" + +msgctxt "#32064" +msgid "Treat DTS like AC3" +msgstr "" + +msgctxt "#32065" +msgid "When any of the force AC3 settings are enabled, treat DTS the same as AC3 (useful for Optical passthrough)" +msgstr "" + +msgctxt "#32066" +msgid "Force audio to AC3" +msgstr "" + +msgctxt "#32067" +msgid "Only force multichannel audio to AC3" +msgstr "" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "" + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "" + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "" + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "" + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "" + +msgctxt "#32201" +msgid "Trailer" +msgstr "" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "" + +msgctxt "#32203" +msgid "Interview" +msgstr "" + +msgctxt "#32204" +msgid "Music Video" +msgstr "" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "" + +msgctxt "#32206" +msgid "Scene" +msgstr "" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "" + +msgctxt "#32209" +msgid "Concert" +msgstr "" + +msgctxt "#32210" +msgid "Featurette" +msgstr "" + +msgctxt "#32211" +msgid "Short" +msgstr "" + +msgctxt "#32212" +msgid "Other" +msgstr "" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "" + +msgctxt "#32303" +msgid "Season" +msgstr "" + +msgctxt "#32304" +msgid "Episode" +msgstr "" + +msgctxt "#32305" +msgid "Extras" +msgstr "" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "" + +msgctxt "#32307" +msgid "More" +msgstr "" + +msgctxt "#32308" +msgid "Available" +msgstr "" + +msgctxt "#32309" +msgid "None" +msgstr "" + +msgctxt "#32310" +msgid "S" +msgstr "" + +msgctxt "#32311" +msgid "E" +msgstr "" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "" + +msgctxt "#32314" +msgid "In Progress" +msgstr "" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "" + +msgctxt "#32316" +msgid "Resume" +msgstr "" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "" + +msgctxt "#32322" +msgid "Delete" +msgstr "" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "" + +msgctxt "#32325" +msgid "Play Next" +msgstr "" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "" + +msgctxt "#32328" +msgid "Yes" +msgstr "" + +msgctxt "#32329" +msgid "No" +msgstr "" + +msgctxt "#32330" +msgid "Message" +msgstr "" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "" + +msgctxt "#32332" +msgid "Home" +msgstr "" + +msgctxt "#32333" +msgid "Playlists" +msgstr "" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "" + +msgctxt "#32336" +msgid "Exit" +msgstr "" + +msgctxt "#32337" +msgid "Cancel" +msgstr "" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "" + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "" + +msgctxt "#32342" +msgid "Switch User" +msgstr "" + +msgctxt "#32343" +msgid "Settings" +msgstr "" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "" + +msgctxt "#32345" +msgid "All" +msgstr "" + +msgctxt "#32346" +msgid "By Name" +msgstr "" + +msgctxt "#32347" +msgid "Artists" +msgstr "" + +msgctxt "#32348" +msgid "Movies" +msgstr "" + +msgctxt "#32349" +msgid "photos" +msgstr "" + +msgctxt "#32350" +msgid "Shows" +msgstr "" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "" + +msgctxt "#32352" +msgid "Date Added" +msgstr "" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "" + +msgctxt "#32354" +msgid "Release Date" +msgstr "" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "" + +msgctxt "#32357" +msgid "By Name" +msgstr "" + +msgctxt "#32358" +msgid "Name" +msgstr "" + +msgctxt "#32359" +msgid "By Rating" +msgstr "" + +msgctxt "#32360" +msgid "Rating" +msgstr "" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "" + +msgctxt "#32362" +msgid "Resolution" +msgstr "" + +msgctxt "#32363" +msgid "By Duration" +msgstr "" + +msgctxt "#32364" +msgid "Duration" +msgstr "" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "" + +msgctxt "#32366" +msgid "First Aired" +msgstr "" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "" + +msgctxt "#32370" +msgid "Date Played" +msgstr "" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "" + +msgctxt "#32372" +msgid "Play Count" +msgstr "" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "" + +msgctxt "#32375" +msgid "No filters available" +msgstr "" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "" + +msgctxt "#32377" +msgid "Year" +msgstr "" + +msgctxt "#32378" +msgid "Decade" +msgstr "" + +msgctxt "#32379" +msgid "Genre" +msgstr "" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "" + +msgctxt "#32381" +msgid "Network" +msgstr "" + +msgctxt "#32382" +msgid "Collection" +msgstr "" + +msgctxt "#32383" +msgid "Director" +msgstr "" + +msgctxt "#32384" +msgid "Actor" +msgstr "" + +msgctxt "#32385" +msgid "Country" +msgstr "" + +msgctxt "#32386" +msgid "Studio" +msgstr "" + +msgctxt "#32387" +msgid "Labels" +msgstr "" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "" + +msgctxt "#32390" +msgid "Aperture" +msgstr "" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "" + +msgctxt "#32392" +msgid "Lens" +msgstr "" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "" + +msgctxt "#32394" +msgid "Music" +msgstr "" + +msgctxt "#32395" +msgid "Audio" +msgstr "" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "" + +msgctxt "#32397" +msgid "Quality" +msgstr "" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "" + +msgctxt "#32401" +msgid "Directors" +msgstr "" + +msgctxt "#32402" +msgid "Writer" +msgstr "" + +msgctxt "#32403" +msgid "Writers" +msgstr "" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "" + +msgctxt "#32411" +msgid "Unknown" +msgstr "" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "" + +msgctxt "#32414" +msgid "Clear" +msgstr "" + +msgctxt "#32415" +msgid "Edit" +msgstr "" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "" + +msgctxt "#32418" +msgid "Creator" +msgstr "" + +msgctxt "#32419" +msgid "Cast" +msgstr "" + +msgctxt "#32420" +msgid "Disc" +msgstr "" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "" + +msgctxt "#32422" +msgid "Exit" +msgstr "" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "" + +msgctxt "#32424" +msgid "Suspend" +msgstr "" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "" + +msgctxt "#32426" +msgid "Reboot" +msgstr "" + +msgctxt "#32427" +msgid "Failed" +msgstr "" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "" + +msgctxt "#32430" +msgid "Discovery" +msgstr "" + +msgctxt "#32431" +msgid "Search" +msgstr "" + +msgctxt "#32432" +msgid "Space" +msgstr "" + +msgctxt "#32433" +msgid "Clear" +msgstr "" + +msgctxt "#32434" +msgid "Searching..." +msgstr "" + +msgctxt "#32435" +msgid "No Results" +msgstr "" + +msgctxt "#32436" +msgid "Paused" +msgstr "" + +msgctxt "#32437" +msgid "Welcome" +msgstr "" + +msgctxt "#32438" +msgid "Previous" +msgstr "" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "" + +msgctxt "#32440" +msgid "On Deck" +msgstr "" + +msgctxt "#32441" +msgid "Unknown" +msgstr "" + +msgctxt "#32442" +msgid "Embedded" +msgstr "" + +msgctxt "#32443" +msgid "Forced" +msgstr "" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "" + +msgctxt "#32445" +msgid "Mono" +msgstr "" + +msgctxt "#32446" +msgid "Stereo" +msgstr "" + +msgctxt "#32447" +msgid "None" +msgstr "" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "" + +msgctxt "#32450" +msgid "Choose Version" +msgstr "" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "" + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "" + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "" + +msgctxt "#32456" +msgid "Show" +msgstr "" + +msgctxt "#32457" +msgid "By Show" +msgstr "" + +msgctxt "#32458" +msgid "Episodes" +msgstr "" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "" + +msgctxt "#32460" +msgid "Sign In" +msgstr "" + +msgctxt "#32461" +msgid "Albums" +msgstr "" + +msgctxt "#32462" +msgid "Artist" +msgstr "" + +msgctxt "#32463" +msgid "By Artist" +msgstr "" + +msgctxt "#32464" +msgid "Player" +msgstr "" + +msgctxt "#32465" +msgid "Use skip step settings from Kodi" +msgstr "" + +msgctxt "#32466" +msgid "Automatically seek selected position after a delay" +msgstr "" + +msgctxt "#32467" +msgid "User Interface" +msgstr "" + +msgctxt "#32468" +msgid "Show dynamic background art" +msgstr "" + +msgctxt "#32469" +msgid "Background art blur amount" +msgstr "" + +msgctxt "#32470" +msgid "Background art opacity" +msgstr "" + +msgctxt "#32471" +msgid "Use Plex/Kodi steps for timeline" +msgstr "" + +msgctxt "#32480" +msgid "Theme music" +msgstr "" + +msgctxt "#32481" +msgid "Off" +msgstr "" + +msgctxt "#32482" +msgid "%(percentage)s %%" +msgstr "" + +msgctxt "#32483" +msgid "Hide Stream Info" +msgstr "" + +msgctxt "#32484" +msgid "Show Stream Info" +msgstr "" + +msgctxt "#32485" +msgid "Go back instantly with the previous menu action in scrolled views" +msgstr "" + +msgctxt "#32487" +msgid "Seek Delay" +msgstr "" + +msgctxt "#32488" +msgid "Screensaver" +msgstr "" + +msgctxt "#32489" +msgid "Quiz Mode" +msgstr "" + +msgctxt "#32490" +msgid "Collections" +msgstr "" + +msgctxt "#32491" +msgid "Folders" +msgstr "" + +msgctxt "#32492" +msgid "Kodi Subtitle Settings" +msgstr "" + +msgctxt "#32493" +msgid "When a media file has a forced/foreign subtitle for a subtitle-enabled language, the Plex Media Server preselects it. This behaviour is usually not necessary and not configurable. This setting fixes that by ignoring the PMSs decision and selecting the same language without a forced flag if possible." +msgstr "" + +msgctxt "#32495" +msgid "Skip intro" +msgstr "" + +msgctxt "#32496" +msgid "Skip credits" +msgstr "" + + +msgctxt "#32500" +msgid "Always show post-play screen (even for short videos)" +msgstr "" + +msgctxt "#32501" +msgid "Time-to-wait between videos on post-play" +msgstr "" + +msgctxt "#32505" +msgid "Visit media in video playlist instead of playing it" +msgstr "" + +msgctxt "#32521" +msgid "Skip Intro Button Timeout" +msgstr "" + +msgctxt "#32522" +msgid "Automatically Skip Intro" +msgstr "" + +msgctxt "#32523" +msgid "Automatically skip intros if available. Doesn't override enabled binge mode." +msgstr "" + +msgctxt "#32524" +msgid "Set how long the skip intro button shows for." +msgstr "" + +msgctxt "#32525" +msgid "Skip Credits Button Timeout" +msgstr "" + +msgctxt "#32526" +msgid "Automatically Skip Credits" +msgstr "" + +msgctxt "#32527" +msgid "Automatically skip credits if available. Doesn't override enabled binge mode." +msgstr "" + +msgctxt "#32528" +msgid "Set how long the skip credits button shows for." +msgstr "" + +msgctxt "#32540" +msgid "Show when the current video will end in player" +msgstr "" + +msgctxt "#32541" +msgid "Shows time left and at which time the media will end." +msgstr "" + +msgctxt "#32542" +msgid "Show \"Ends at\" label for the end-time as well" +msgstr "" + +msgctxt "#32543" +msgid "Ends at" +msgstr "" + +msgctxt "#32601" +msgid "Allow AV1" +msgstr "" + +msgctxt "#32602" +msgid "Enable this if your hardware can handle AV1. Disable it to force transcoding." +msgstr "" + +msgctxt "#33101" +msgid "By Audience Rating" +msgstr "" + +msgctxt "#33102" +msgid "Audience Rating" +msgstr "" + +msgctxt "#33103" +msgid "By my Rating" +msgstr "" + +msgctxt "#33104" +msgid "My Rating" +msgstr "" + +msgctxt "#33105" +msgid "By Content Rating" +msgstr "" + +msgctxt "#33106" +msgid "Content Rating" +msgstr "" + +msgctxt "#33107" +msgid "By Critic Rating" +msgstr "" + +msgctxt "#33108" +msgid "Critic Rating" +msgstr "" + +msgctxt "#33200" +msgid "Background Color" +msgstr "" + +msgctxt "#33201" +msgid "Specify solid Background Color instead of using media images" +msgstr "" + +msgctxt "#33400" +msgid "Use old compatibility profile" +msgstr "" + +msgctxt "#33401" +msgid "Uses the Chrome client profile instead of the custom one. Might fix rare issues with 3D playback." +msgstr "" + +msgctxt "#33501" +msgid "Video played threshold" +msgstr "" + +msgctxt "#33502" +msgid "Set this to the same value as your Plex server (Settings>Library>Video played threshold) to avoid certain pitfalls, Default: 90 %" +msgstr "" + +msgctxt "#33503" +msgid "Use alternative hubs refresh" +msgstr "" + +msgctxt "#33504" +msgid "Refreshes all hubs for all libraries after an item's watch-state has changed, instead of only those likely affected. Use this if you find a hub that doesn't update properly." +msgstr "" + +msgctxt "#33505" +msgid "Show intro skip button early" +msgstr "" + +msgctxt "#33506" +msgid "Show the intro skip button from the start of a video with an intro marker. The auto-skipping setting applies. Doesn\'t override enabled binge mode." +msgstr "" + +msgctxt "#33507" +msgid "Enabled" +msgstr "" + +msgctxt "#33508" +msgid "Disabled" +msgstr "" + +msgctxt "#33509" +msgid "Early intro skip threshold (default: < 60s/1m)" +msgstr "" + +msgctxt "#33510" +msgid "When showing the intro skip button early, only do so if the intro occurs within the first X seconds." +msgstr "" + +msgctxt "#33600" +msgid "System" +msgstr "" + +msgctxt "#33601" +msgid "Show video chapters" +msgstr "" + +msgctxt "#33602" +msgid "If available, show video chapters from the video-file instead of the timeline-big-seek-steps." +msgstr "" + +msgctxt "#33603" +msgid "Use virtual chapters" +msgstr "" + +msgctxt "#33604" +msgid "When the above is enabled and no video chapters are available, simulate them by using the markers identified by the Plex Server (Intro, Credits)." +msgstr "" + +msgctxt "#33605" +msgid "Video Chapters" +msgstr "" + +msgctxt "#33606" +msgid "Virtual Chapters" +msgstr "" + +msgctxt "#33607" +msgid "Chapter {}" +msgstr "" + +msgctxt "#33608" +msgid "Intro" +msgstr "" + +msgctxt "#33609" +msgid "Credits" +msgstr "" + +msgctxt "#33610" +msgid "Main" +msgstr "" + +msgctxt "#33611" +msgid "Chapters" +msgstr "" + +msgctxt "#33612" +msgid "Markers" +msgstr "" + +msgctxt "#33613" +msgid "Kodi Buffer Size (MB)" +msgstr "" + +msgctxt "#33614" +msgid "Set the Kodi Cache/Buffer size. Free: {} MB, Recommended: ~100 MB, Recommended max: {} MB, Default: 20 MB." +msgstr "" + +msgctxt "#33615" +msgid "{time} left" +msgstr "" + +msgctxt "#33616" +msgid "Addon Path" +msgstr "" + +msgctxt "#33617" +msgid "Userdata/Profile Path" +msgstr "" + +msgctxt "#33618" +msgid "TV binge-viewing mode" +msgstr "" + +msgctxt "#33619" +msgid "Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t skip the intro of the first episode of a season.\n\nCan be disabled/enabled per TV show." +msgstr "" + +msgctxt "#33620" +msgid "Plex requests timeout (seconds)" +msgstr "" + +msgctxt "#33621" +msgid "Set the (async and connection) timeout value of the Python requests library in seconds. Default: 5" +msgstr "" + +msgctxt "#33622" +msgid "LAN reachability timeout (ms)" +msgstr "" + +msgctxt "#33623" +msgid "When checking for LAN reachability, use this timeout. Default: 10ms" +msgstr "" + +msgctxt "#33624" +msgid "Network" +msgstr "" + +msgctxt "#33625" +msgid "Smart LAN/local server discovery" +msgstr "" + +msgctxt "#33626" +msgid "Checks whether servers returned from Plex.tv are actually local/in your LAN. For specific setups (e.g. Docker) Plex.tv might not properly detect a local server.\n\nNOTE: Only works on Kodi 19 or above." +msgstr "" + +msgctxt "#33627" +msgid "Prefer LAN/local servers over security" +msgstr "" + +msgctxt "#33628" +msgid "Prioritizes local connections over secure ones. Needs the proper setting in \"Allow Insecure Connections\" and the Plex Server's \"Secure connections\" at \"Preferred\". Can be used to enforce manual servers." +msgstr "" + +msgctxt "#33629" +msgid "Auto-skip intro/credits offset" +msgstr "" + +msgctxt "#33630" +msgid "Intro/credits markers might be a little early in Plex. When auto skipping add (or subtract) this many seconds from the marker. This avoids cutting off content, while possibly skipping the marker a little late." +msgstr "" + +msgctxt "#32631" +msgid "Playback (user-specific)" +msgstr "" + +msgctxt "#33632" +msgid "Server connectivity check timeout (seconds)" +msgstr "" + +msgctxt "#33633" +msgid "Set the maximum amount of time a server connection has to answer a connectivity request. Default: 2.5" +msgstr "" + +msgctxt "#33634" +msgid "Combined Chapters" +msgstr "" + +msgctxt "#33635" +msgid "Final Credits" +msgstr "" + +msgctxt "#32700" +msgid "Action on Sleep event" +msgstr "" + +msgctxt "#32701" +msgid "When Kodi receives a sleep event from the system, run the following action." +msgstr "" + +msgctxt "#32702" +msgid "Nothing" +msgstr "" + +msgctxt "#32703" +msgid "Stop playback" +msgstr "" + +msgctxt "#32704" +msgid "Quit Kodi" +msgstr "" + +msgctxt "#32705" +msgid "CEC Standby" +msgstr "" + +msgctxt "#32800" +msgid "Skipping intro" +msgstr "" + +msgctxt "#32801" +msgid "Skipping credits" +msgstr "" + +msgctxt "#32900" +msgid "While playing back an item and seeking on the seekbar, automatically seek to the selected position after a delay instead of having to confirm the selection." +msgstr "" + +msgctxt "#32901" +msgid "Seek delay in seconds." +msgstr "" + +msgctxt "#32902" +msgid "Kodi has its own skip step settings. Try to use them if they're configured instead of the default ones." +msgstr "" + +msgctxt "#32903" +msgid "Use the above for seeking on the timeline as well." +msgstr "" + +msgctxt "#32904" +msgid "In seconds." +msgstr "" + +msgctxt "#32905" +msgid "Cancel post-play timer by pressing OK/SELECT" +msgstr "" + +msgctxt "#32906" +msgid "Cancel skip marker timer with BACK" +msgstr "" + +msgctxt "#32907" +msgid "When auto-skipping a marker, allow cancelling the timer by pressing BACK." +msgstr "" + +msgctxt "#32908" +msgid "Immediately skip marker with OK/SELECT" +msgstr "" + +msgctxt "#32909" +msgid "When auto-skipping a marker with a timer, allow skipping immediately by pressing OK/SELECT." +msgstr "" + +msgctxt "#32912" +msgid "Show buffer-state on timeline" +msgstr "" + +msgctxt "#32913" +msgid "Shows the current Kodi buffer/cache state on the video player timeline." +msgstr "" + +msgctxt "#32914" +msgid "Loading" +msgstr "" + +msgctxt "#32915" +msgid "Slow connection" +msgstr "" + +msgctxt "#32916" +msgid "Use with a wonky/slow connection, e.g. in a hotel room. Adjusts the UI to visually wait for item refreshes and waits for the buffer to fill when starting playback. Automatically sets readfactor=20, requires Kodi restart." +msgstr "" + +msgctxt "#32917" +msgid "Couldn't fill buffer in time ({}s)" +msgstr "" + +msgctxt "#32918" +msgid "Buffer wait timeout (seconds)" +msgstr "" + +msgctxt "#32919" +msgid "When slow connection is enabled in the addon, wait this long for the buffer to fill. Default: 120 s" +msgstr "" + +msgctxt "#32920" +msgid "Insufficient buffer wait (seconds)" +msgstr "" + +msgctxt "#32921" +msgid "When slow connection is enabled in the addon and the configured buffer isn't big enough for us to determine its fill state, wait this long when starting playback. Default: 10 s" +msgstr "" + +msgctxt "#32922" +msgid "Kodi Cache Readfactor" +msgstr "" + +msgctxt "#32923" +msgid "Sets the Kodi cache readfactor value. Default: {0}, recommended: {1}. With \"Slow connection\" enabled this will be set to {2}, as otherwise the cache doesn't fill fast/aggressively enough." +msgstr "" + +msgctxt "#32924" +msgid "Minimize" +msgstr "" + +msgctxt "#32925" +msgid "Playback Settings" +msgstr "" + +msgctxt "#32926" +msgid "Wrong pin entered!" +msgstr "" + +msgctxt "#32927" +msgid "Use episode thumbnails in continue hub" +msgstr "" + +msgctxt "#32928" +msgid "Instead of using media artwork, use thumbnails for episodes in the continue hub on the home screen if available." +msgstr "" + +msgctxt "#32929" +msgid "Use legacy background fallback image" +msgstr "" + +msgctxt "#32930" +msgid "Previous Subtitle" +msgstr "" + +msgctxt "#32931" +msgid "Audio/Subtitles" +msgstr "" + +msgctxt "#32932" +msgid "Show subtitle quick-actions button" +msgstr "" + +msgctxt "#32933" +msgid "Show FFWD/RWD buttons" +msgstr "" + +msgctxt "#32934" +msgid "Show repeat button" +msgstr "" + +msgctxt "#32935" +msgid "Show shuffle button" +msgstr "" + +msgctxt "#32936" +msgid "Show playlist button" +msgstr "" + +msgctxt "#32937" +msgid "Show prev/next button" +msgstr "" + +msgctxt "#32938" +msgid "Only for Episodes" +msgstr "" + +msgctxt "#32939" +msgid "Only applies to video player UI" +msgstr "" + +msgctxt "#32940" +msgid "Player UI" +msgstr "" + +msgctxt "#32941" +msgid "Forced subtitles fix" +msgstr "" + +msgctxt "#32942" +msgid "Other seasons" +msgstr "" + +msgctxt "#32943" +msgid "Crossfade dynamic background art" +msgstr "" + +msgctxt "#32944" +msgid "Burn-in SSA subtitles (DirectStream)" +msgstr "" + +msgctxt "#32945" +msgid "When Direct Streaming instruct the Plex Server to burn in SSA/ASS subtitles (thus transcoding the video stream). If disabled it will not touch the video stream, but will convert the subtitle to unstyled text." +msgstr "" + +msgctxt "#32946" +msgid "Stop video playback on idle after" +msgstr "" + +msgctxt "#32947" +msgid "Stop video playback on screensaver" +msgstr "" + +msgctxt "#32948" +msgid "Allow auto-skip when transcoding" +msgstr "" + +msgctxt "#32949" +msgid "When transcoding/DirectStreaming, allow auto-skip functionality." +msgstr "" + +msgctxt "#32950" +msgid "Use extended title for subtitles" +msgstr "" + +msgctxt "#32951" +msgid "When displaying subtitles use the extendedDisplayTitle Plex exposes." +msgstr "" + +msgctxt "#32952" +msgid "Dialog flicker fix" +msgstr "" + +msgctxt "#32953" +msgid "Reviews" +msgstr "" + +msgctxt "#32954" +msgid "Needs Kodi restart. WARNING: This will overwrite advancedsettings.xml!\n\nTo customize other cache/network-related values, copy \"script.plexmod/pm4k_cache_template.xml\" to profile folder and edit it to your liking. (See About section for the file paths)" +msgstr "" + +msgctxt "#32955" +msgid "Use Kodi keyboard for searching" +msgstr "" + +msgctxt "#32956" +msgid "Poster resolution scaling %" +msgstr "" + +msgctxt "#32957" +msgid "In percent. Scales the resolution of all posters/thumbnails for better image quality. May impact PMS/PM4K performance, will increase the cache usage accordingly. Recommended: 200-300 % for for big screens if your hardware can handle it. Needs addon restart." +msgstr "" + +msgctxt "#32958" +msgid "Calculate OpenSubtitles.com hash" +msgstr "" + +msgctxt "#32959" +msgid "When opening the subtitle download feature, automatically calculate the OpenSubtitles.com hash for the given file. Can improve search results, downloads 2*64 KB of the video file to calculate the hash." +msgstr "" diff --git a/script.plexmod/resources/language/resource.language.es_es/strings.po b/script.plexmod/resources/language/resource.language.es_es/strings.po new file mode 100644 index 000000000..b8c6adca8 --- /dev/null +++ b/script.plexmod/resources/language/resource.language.es_es/strings.po @@ -0,0 +1,951 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2020-05-16 10:52+0200\n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: \n" +"X-Generator: Poedit 2.0.2\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Principal" + +msgctxt "#32001" +msgid "Original" +msgstr "Original" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mbps 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mbps 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mbps 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mbps 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mbps 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mbps 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mbps 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mbps 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kbps" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kbps" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kbps" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kbps" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kbps" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Calidad Local" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Calidad Remoto" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Calidad Online" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Formato transcodificación" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Log Depuración" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Permitir Reproducción en Directo" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Permitir Stream en Directo" + +msgctxt "#32027" +msgid "Force" +msgstr "Forzar" + +msgctxt "#32028" +msgid "Always" +msgstr "Siempre" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Solo formatos de imagen" + +msgctxt "#32030" +msgid "Auto" +msgstr "Auto" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Subtítulos (Solo Reproducción Directo)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Permetir conexiones inseguras" + +msgctxt "#32033" +msgid "Never" +msgstr "Nunca" + +msgctxt "#32034" +msgid "On Same network" +msgstr "En la misma red" + +msgctxt "#32035" +msgid "Always" +msgstr "Siempre" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Permitir 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Permetir HEVC (h265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Accesso automatico" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Post Play Auto Play" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Activar la descarga de subtítulos" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Activar la descarga de subtítulos" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Detección del Servidor (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Arrancar Plex al iniciar Kodi" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "IP Conexión 1" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Puerto Conexión 1" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "IP Conexión 2" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Puerto conexión 2" + +msgctxt "#32048" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Avanzar" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Servidores manuales" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Privacidad" + +msgctxt "#32052" +msgid "About" +msgstr "Acerca de" + +msgctxt "#32053" +msgid "Video" +msgstr "Video" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Versión Add-on" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Versión Kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Resolución de Pantalla" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Versión del Servidor" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Omitir selección de usuario y PIN al iniciar." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Si se activa, cuando acabe la reproducción y 'Siguiente Capítulo' esté disponible, se reproducirá automáticamente a los 15 segundos." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Activa esto si se puede reproducir contenido 4K. Desactívalo para forzar la trasncodificación." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Activa esto si se puede reproducir HEVC/h265. Desactiva para forzar la trascodificación." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Conectar a servidores con conexiones no seguras.[CR][CR]* [B]Nunca[/B]: Nunca conectarse a un servidor de forma no segura[CR]* [B]En la misma red[/B]: Permitir para la misma red[CR]* [B]Siempre[/B]: Permitir para todas las conexiones" + +msgctxt "#32201" +msgid "Trailer" +msgstr "Tráiler" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Escenas eliminadas" + +msgctxt "#32203" +msgid "Interview" +msgstr "Entrevista" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Video musical" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Detrás de las escenas" + +msgctxt "#32206" +msgid "Scene" +msgstr "Escena" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Videos de música en vivo" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Videos de letras musicales" + +msgctxt "#32209" +msgid "Concert" +msgstr "Conciertos" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Featurette" + +msgctxt "#32211" +msgid "Short" +msgstr "Cortos" + +msgctxt "#32212" +msgid "Other" +msgstr "Otros" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Ir a Album" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Ir a Artistas" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Ir a {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Temporada" + +msgctxt "#32304" +msgid "Episode" +msgstr "Capítulo" + +msgctxt "#32305" +msgid "Extras" +msgstr "Extras" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Series relacionadas" + +msgctxt "#32307" +msgid "More" +msgstr "Más" + +msgctxt "#32308" +msgid "Available" +msgstr "Disponible" + +msgctxt "#32309" +msgid "None" +msgstr "Ninguno" + +msgctxt "#32310" +msgid "S" +msgstr "S" + +msgctxt "#32311" +msgid "E" +msgstr "E" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "No disponible" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Este elemento no está disponible actualmente" + +msgctxt "#32314" +msgid "In Progress" +msgstr "En curso" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "¿Continuar reproducción?" + +msgctxt "#32316" +msgid "Resume" +msgstr "Continuar" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Reproducir desde el principio" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Marcar como no visto" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Marcar como visto" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Marca la Temporada como no vista" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Marcar la Temporada como vista" + +msgctxt "#32322" +msgid "Delete" +msgstr "Borrar" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Ir a la Serie" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Ir a {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Reproducir el siguiente" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "¿Borrarlo de verdad?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "¿Seguro que quieres borrar este medio?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Sí" + +msgctxt "#32329" +msgid "No" +msgstr "No" + +msgctxt "#32330" +msgid "Message" +msgstr "Mensaje" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Hubo un problema al intentar borrar este medio." + +msgctxt "#32332" +msgid "Home" +msgstr "Inicio" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Listas de reproducción" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Salir realmente" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "¿Listo para salir de Plex?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Salir" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Cancelar" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Ningún servidor encontrado" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Servidor no accessible" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Tests de conexión en curso. Espere por favor." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Servidor no accessible. Por favor, comprueba la conexión de tu servidor." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Cambiar usuario" + +msgctxt "#32343" +msgid "Settings" +msgstr "Configuración" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Desconectar" + +msgctxt "#32345" +msgid "All" +msgstr "Todo" + +msgctxt "#32346" +msgid "By Name" +msgstr "Por Nombre" + +msgctxt "#32347" +msgid "Artists" +msgstr "Artistas" + +msgctxt "#32348" +msgid "movies" +msgstr "películas" + +msgctxt "#32349" +msgid "photos" +msgstr "fotos" + +msgctxt "#32350" +msgid "Shows" +msgstr "Series" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Por fecha" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Fecha" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Por fecha de estreno" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Fecha de estreno" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Por fecha de visionado" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Fecha de visionado" + +msgctxt "#32357" +msgid "By Name" +msgstr "Por nombre" + +msgctxt "#32358" +msgid "Name" +msgstr "Nombre" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Por valoración" + +msgctxt "#32360" +msgid "Rating" +msgstr "Valoración" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Por Resolución" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Resolución" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Por Duración" + +msgctxt "#32364" +msgid "Duration" +msgstr "Duración" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Por primera emisión" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Primera emisión" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Por No Vistos" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Non visto" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Por Fecha de Reproducción" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Fecha de Reproducción" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Por Número de Reproducciones" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Número de Reproducciones" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Por Fecha de Captura" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Fecha de Captura" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Sin filtros disponbles" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Quitar filtros" + +msgctxt "#32377" +msgid "Year" +msgstr "Año" + +msgctxt "#32378" +msgid "Decade" +msgstr "Década" + +msgctxt "#32379" +msgid "Genre" +msgstr "Género" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Clasificación por Edad" + +msgctxt "#32381" +msgid "Network" +msgstr "Red" + +msgctxt "#32382" +msgid "Collection" +msgstr "Colección" + +msgctxt "#32383" +msgid "Director" +msgstr "Dirección" + +msgctxt "#32384" +msgid "Actor" +msgstr "Actores" + +msgctxt "#32385" +msgid "Country" +msgstr "País" + +msgctxt "#32386" +msgid "Studio" +msgstr "Estudio" + +msgctxt "#32387" +msgid "Labels" +msgstr "Etiquetas" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Marca de Cámara" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Modelo de Cámera" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Apertura" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Velocidad de apertura" + +msgctxt "#32392" +msgid "Lens" +msgstr "Lente" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Series de TV" + +msgctxt "#32394" +msgid "Music" +msgstr "Música" + +msgctxt "#32395" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Subtítulos" + +msgctxt "#32397" +msgid "Quality" +msgstr "Calidad" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Configuración de Video de Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Configuración de Audio de Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Ir a Temporada" + +msgctxt "#32401" +msgid "Directors" +msgstr "Dirección" + +msgctxt "#32402" +msgid "Writer" +msgstr "Autor" + +msgctxt "#32403" +msgid "Writers" +msgstr "Guionistas" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Películas relacionadas" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Descargar Subtítulos" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Retardo en Subtítulos" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Siguiente Subtítulo" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Desactivar Subtítulos" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Activar Subtítulos" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Versión de la Plataforma" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Desconocido" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Modificar o Borrar" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "¿Editar la direeción IP o borrar la configuración?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Borrar" + +msgctxt "#32415" +msgid "Edit" +msgstr "Modificar" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Introduce la Dirección IP" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Introduce el Puerto" + +msgctxt "#32418" +msgid "Creator" +msgstr "Creador" + +msgctxt "#32419" +msgid "Cast" +msgstr "Elenco" + +msgctxt "#32420" +msgid "Disc" +msgstr "Disco" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Desconectar" + +msgctxt "#32422" +msgid "Exit" +msgstr "Salir" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Apagar" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Suspender" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Hibernar" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Reiniciar" + +msgctxt "#32427" +msgid "Failed" +msgstr "Fallo" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "¡El acceso falló!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Continuar desde {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Descubrimiento" + +msgctxt "#32431" +msgid "Search" +msgstr "Búsqueda" + +msgctxt "#32432" +msgid "Space" +msgstr "Espacio" + +msgctxt "#32433" +msgid "Clear" +msgstr "Borrar" + +msgctxt "#32434" +msgid "Searching..." +msgstr "Buscando..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Sin resultados" + +msgctxt "#32436" +msgid "Paused" +msgstr "En Pausa" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Bienvenido" + +msgctxt "#32438" +msgid "Previous" +msgstr "Anterior" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Reproducir el Siguiente" + +msgctxt "#32440" +msgid "On Deck" +msgstr "En Portada" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Desconocido" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Integrado" + +msgctxt "#32443" +msgid "Forced" +msgstr "Forzado" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Letras" + +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Stereo" + +msgctxt "#32447" +msgid "None" +msgstr "Ninguno" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "¡La reproducción falló!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "No pude conectarme a plex.tv[CR]Comprueba la conexión y vuelve a intentarlo." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Elegir una Versión" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Reproduce un Versión..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "No hay contenido disponible en esta bilioteca" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Por favo, añade contenido y/o comprueba que 'Incluir en el dashboard' esté activado." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Sin contenido disponible por este filtro" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Por favor, cambie o quite el filtro actual" + +msgctxt "#32456" +msgid "Show" +msgstr "Serie" + +msgctxt "#32457" +msgid "By Show" +msgstr "Por Serie" + +msgctxt "#32458" +msgid "Episodes" +msgstr "Capítulo" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Modo Offline" + +msgctxt "#32460" +msgid "Sign In" +msgstr "Acceso" + +msgctxt "#32461" +msgid "Albums" +msgstr "Albums" + +msgctxt "#32462" +msgid "Artist" +msgstr "Artista" + +msgctxt "#32463" +msgid "By Artist" +msgstr "Por Artista" diff --git a/script.plexmod/resources/language/resource.language.fr_fr/strings.po b/script.plexmod/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 000000000..b4ec81eca --- /dev/null +++ b/script.plexmod/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,953 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2017-05-29 16:47++0200\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Main" + +msgctxt "#32001" +msgid "Original" +msgstr "Original" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mbps 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mbps 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mbps 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mbps 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mbps 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mbps 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mbps 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mbps 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kbps" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kbps" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kbps" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kbps" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kbps" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Qualité locale" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Qualité distante" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Qualité online" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Format transcodage" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Debug Logging" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Autoriser Direct Play" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Autoriser Direct Stream" + +msgctxt "#32027" +msgid "Force" +msgstr "Force" + +msgctxt "#32028" +msgid "Always" +msgstr "Toujours" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Seulement formats d'image" + +msgctxt "#32030" +msgid "Auto" +msgstr "Auto" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Incruster sous-titres (pour Direct Play)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Autoriser connexions non sécurisées" + +msgctxt "#32033" +msgid "Never" +msgstr "Jamais" + +msgctxt "#32034" +msgid "On Same network" +msgstr "Sur le même réseau" + +msgctxt "#32035" +msgid "Always" +msgstr "Toujours" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Autoriser 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Autoriser HEVC (h265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Connexion automatique" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Post Play Auto Play" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Activer le téléchargement des sous-titres" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Activer le téléchargement des sous-titres" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Server Discovery (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Démarrer Plex au démarrage de Kodi" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "Connexion 1 IP" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Connexion 1 Port" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "Connexion 2 IP" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Connexion 2 Port" + +msgctxt "#32048" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Avancée" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Serveurs manuels" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Privacy" + +msgctxt "#32052" +msgid "About" +msgstr "A propos" + +msgctxt "#32053" +msgid "Video" +msgstr "Video" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Version de l'addon" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Version de kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Résolution de l'écran" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Version actuelle du serveur" + + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Passer l'écran de sélection de l'utilisateur et du PIN au démarrage" + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Si actif, à la fin de la lecture, les éléments suivants seront joués après un délai de 15s." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Activer cette option si votre hardware est capable de lire la 4K. Désactiver pour forcer le transcodage." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Activer cette option si votre hardware est capable de décoder l'HEVC/h265. Désactiver pour forcer le transcodage." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Pour se connecter à un serveur via une connexion non sécurisée.[CR][CR]* [B]Attention[/B]: Ne jamais se connecter à un serveur via une connexion son sécurisée[CR]* [B]Sur le même réseau[/B]: Autoriser si même réseau[CR]* [B]Toujours[/B]: Autoriser même réseau et connexions distantes" + + +msgctxt "#32201" +msgid "Trailer" +msgstr "Bande-annonce" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Supprimer scène" + +msgctxt "#32203" +msgid "Interview" +msgstr "Interview" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Vidéos Musiques" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Dans les coulisses" + +msgctxt "#32206" +msgid "Scene" +msgstr "Scene" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Vidéos Musiques Live" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Paroles vidéos musiques" + +msgctxt "#32209" +msgid "Concert" +msgstr "Concert" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Featurette" + +msgctxt "#32211" +msgid "Short" +msgstr "Courte" + +msgctxt "#32212" +msgid "Other" +msgstr "Autres" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Voir l'album" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Voir l'artiste" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Aller à {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Saison" + +msgctxt "#32304" +msgid "Episode" +msgstr "Episode" + +msgctxt "#32305" +msgid "Extras" +msgstr "Extras" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Liens" + +msgctxt "#32307" +msgid "More" +msgstr "Plus" + +msgctxt "#32308" +msgid "Available" +msgstr "Disponible" + +msgctxt "#32309" +msgid "None" +msgstr "Aucun" + +msgctxt "#32310" +msgid "S" +msgstr "S" + +msgctxt "#32311" +msgid "E" +msgstr "E" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Non disponible" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Cet élément n'est actuellement pas disponible." + +msgctxt "#32314" +msgid "In Progress" +msgstr "En cours" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Poursuivre la lecture ?" + +msgctxt "#32316" +msgid "Resume" +msgstr "Resume" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Lire depuis le début" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Marquer comme non vu" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Marquer comme vu" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Marquer saison comme non vue" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Marquer saison comme vue" + +msgctxt "#32322" +msgid "Delete" +msgstr "Supprimer" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Voir le Show" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Aller à {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Lire le media suivant" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Supprimer?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Etes-vous sûr de vouloir supprimer ce media ?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Oui" + +msgctxt "#32329" +msgid "No" +msgstr "Non" + +msgctxt "#32330" +msgid "Message" +msgstr "Message" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Erreur : impossible de supprimer le media" + +msgctxt "#32332" +msgid "Home" +msgstr "Home" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Playlists" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Quitter" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Souhaitez-vous quitter Plex ?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Quitter" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Annuler" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Aucun serveur trouvé" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Serveur non accessible" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Tests de connexion en cours. Merci de patienter." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Serveur non joignable. Merci de vérifier l'accessibilité du serveur." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Changer d'utilisateur" + +msgctxt "#32343" +msgid "Settings" +msgstr "Paramètres" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Déconnexion" + +msgctxt "#32345" +msgid "All" +msgstr "Tous" + +msgctxt "#32346" +msgid "By Name" +msgstr "Par Nom" + +msgctxt "#32347" +msgid "Artists" +msgstr "Artites" + +msgctxt "#32348" +msgid "movies" +msgstr "films" + +msgctxt "#32349" +msgid "photos" +msgstr "photos" + +msgctxt "#32350" +msgid "Shows" +msgstr "Séries" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Par date d'ajout" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Date d'ajout" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Par date de sortie" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Date de sortie" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Par date de visionnage" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Date du visionnage" + +msgctxt "#32357" +msgid "By Name" +msgstr "Par nom" + +msgctxt "#32358" +msgid "Name" +msgstr "Nom" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Par note" + +msgctxt "#32360" +msgid "Rating" +msgstr "Note" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Par qualité" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Qualité" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Par durée" + +msgctxt "#32364" +msgid "Duration" +msgstr "Durée" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Par première diffusion" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Première diffusion" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Par non vus" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Non vus" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Par date de lecture" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Date de lecture" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Par nombre de lectures" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Nombre de lectures" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Par date de prise de vue" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Date de prise de vue" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Aucuns filtres disponibles" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Effacer filtres" + +msgctxt "#32377" +msgid "Year" +msgstr "Année" + +msgctxt "#32378" +msgid "Decade" +msgstr "Décennie" + +msgctxt "#32379" +msgid "Genre" +msgstr "Genre" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Note du contenu" + +msgctxt "#32381" +msgid "Network" +msgstr "Réseau" + +msgctxt "#32382" +msgid "Collection" +msgstr "Collection" + +msgctxt "#32383" +msgid "Director" +msgstr "Réalisateur" + +msgctxt "#32384" +msgid "Actor" +msgstr "Acteur" + +msgctxt "#32385" +msgid "Country" +msgstr "Pays" + +msgctxt "#32386" +msgid "Studio" +msgstr "Studio" + +msgctxt "#32387" +msgid "Labels" +msgstr "Labels" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Marque appareil photo" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Modèle caméra" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Aperture" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Vitesse d'obturation" + +msgctxt "#32392" +msgid "Lens" +msgstr "Lentille" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Séries TV" + +msgctxt "#32394" +msgid "Music" +msgstr "Musiques" + +msgctxt "#32395" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Sous-titres" + +msgctxt "#32397" +msgid "Quality" +msgstr "Qualité" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Paramètres vidéo Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Paramètres audio Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Aller à la saison" + +msgctxt "#32401" +msgid "Directors" +msgstr "Réalisateurs" + +msgctxt "#32402" +msgid "Writer" +msgstr "Auteur" + +msgctxt "#32403" +msgid "Writers" +msgstr "Auteurs" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Films liés" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Télécharger sous-titres" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Délai sous-titres" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Sous-titres suivants" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Désactiver sous-titres" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Activer sous-titres" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Version de la plateforme" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Inconnu" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Editer ou Effacer" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Editer adresse IP ou effacer les paramètres actuels ?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Effacer" + +msgctxt "#32415" +msgid "Edit" +msgstr "Editer" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Saisir adresse IP" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Saisir le port" + +msgctxt "#32418" +msgid "Creator" +msgstr "Creator" + +msgctxt "#32419" +msgid "Cast" +msgstr "Casting" + +msgctxt "#32420" +msgid "Disc" +msgstr "Disc" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Déconnexion" + +msgctxt "#32422" +msgid "Exit" +msgstr "Quitter" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Eteindre" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Veille" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Veille prolongée" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Redémarrer" + +msgctxt "#32427" +msgid "Failed" +msgstr "Echec" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Impossible de se connecter" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Reprendre à {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Recherche" + +msgctxt "#32431" +msgid "Search" +msgstr "Recherche" + +msgctxt "#32432" +msgid "Space" +msgstr "Espace" + +msgctxt "#32433" +msgid "Clear" +msgstr "Effacer" + +msgctxt "#32434" +msgid "Searching..." +msgstr "Recherche en cours..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Aucun résultat" + +msgctxt "#32436" +msgid "Paused" +msgstr "Pause" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Bienvenue" + +msgctxt "#32438" +msgid "Previous" +msgstr "Précédent" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "A regarder ensuite" + +msgctxt "#32440" +msgid "On Deck" +msgstr "Découvrir" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Inconnu" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Intégré" + +msgctxt "#32443" +msgid "Forced" +msgstr "Forcé" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Parole" + +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Stereo" + +msgctxt "#32447" +msgid "None" +msgstr "Aucun" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Echec de lecture!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Impossible de se connecter à plex.tv[CR]Vérifiez votre connexion et réessayez." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Choisir une version" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Lire version..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "" + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Aucun contenu disponible dans cette librairie" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Merci de modifier ou supprimer le filtre courant" + +msgctxt "#32456" +msgid "Show" +msgstr "" + +msgctxt "#32457" +msgid "By Show" +msgstr "" + +msgctxt "#32458" +msgid "Episodes" +msgstr "" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Mode Hors-Ligne" + +msgctxt "#32460" +msgid "Sign In" +msgstr "Se connecter" + +msgctxt "#32461" +msgid "Albums" +msgstr "" + +msgctxt "#32462" +msgid "Artist" +msgstr "" + +msgctxt "#32463" +msgid "By Artist" +msgstr "" + diff --git a/script.plexmod/resources/language/resource.language.hu_hu/strings.po b/script.plexmod/resources/language/resource.language.hu_hu/strings.po new file mode 100644 index 000000000..18410177c --- /dev/null +++ b/script.plexmod/resources/language/resource.language.hu_hu/strings.po @@ -0,0 +1,928 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2017-04-21 19:23+0100\n" +"Last-Translator: Norbert Suto \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: hu\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Főoldal" + +msgctxt "#32001" +msgid "Original" +msgstr "Eredeti" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mbps 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mbps 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mbps 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mbps 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mbps 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mbps 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mbps 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mbps 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kbps" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kbps" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kbps" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kbps" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kbps" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Helyi minőség" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Távoli minőség" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Online minőség" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Átkódolási formátum" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Hibakeresési naplózás" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Közvetlen lejátszás engedélyezése" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Közvetlen adatfolyam engedélyezése" + +msgctxt "#32027" +msgid "Force" +msgstr "Kényszerítés" + +msgctxt "#32028" +msgid "Always" +msgstr "Mindig" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Csak képformátumok esetében" + +msgctxt "#32030" +msgid "Auto" +msgstr "Automatikus" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Feliratok égetése (Csak közvetlen lejátszás esetén)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Nem biztonságos kapcsolatok engedélyezése" + +msgctxt "#32033" +msgid "Never" +msgstr "Soha" + +msgctxt "#32034" +msgid "On Same network" +msgstr "Ugyanazon a hálózaton" + +msgctxt "#32035" +msgid "Always" +msgstr "Mindig" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "4K engedélyezése" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "HEVC (h265) engedélyezése" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Automatikus bejelentkezés" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Post Play Auto Play" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Felirat letöltés engedélyezése" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Felirat letöltés engedélyezése" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Szerver felderítés" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Indítsa el a Plexet a Kodi indításakor" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "1. Kapcsolat IP" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "1. Kapcsolat Port" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "2. Kapcsolat IP" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "2. Kapcsolat Port" + +msgctxt "#32048" +msgid "Audio" +msgstr "Audió" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Fejlett beállítások" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Kézi szerverek" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Adatvédelem" + +msgctxt "#32052" +msgid "About" +msgstr "Rólunk" + +msgctxt "#32053" +msgid "Video" +msgstr "Videó" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Addon verzió" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Kodi verzió" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Képernyőfelbontás" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Jelenlegi szerver verzió" + + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Felhasználó kiválasztás és a PIN kód megadás átugrása indításkor." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Ha engedélyezi, akkor a lejátszás befejezése után, amennyiben rendelkezésre áll egy \"következő\" elem, az 15 másodperces késleltetés után automatikusan lejátszásra kerül." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Engedélyezze ezt, ha a hardver képes a 4K lejátszásra. Kapcsolja ki, amennyiben kényszeríteni szeretné az átkódolást." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Engedélyezze ezt, ha a hardver képes a HEVC/h265 lejátszásra. Kapcsolja ki, amennyiben kényszeríteni szeretné az átkódolást." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Mikor kapcsolódhat nem biztonságosan a szerverekhez.[CR][CR]* [B]Soha[/B]: Soha ne kapcsolódjon[CR]* [B]Azonos hálózat[/B]: Azonos hálózat esetén engedélyezze[CR]* [B]Mindig[/B]: Engedélyezze helyi és távoli kapcsolat esetén is" + + +msgctxt "#32201" +msgid "Trailer" +msgstr "Előzetes" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Törölt jelenet" + +msgctxt "#32203" +msgid "Interview" +msgstr "Interjú" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Videóklip" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "A kulisszák mögött" + +msgctxt "#32206" +msgid "Scene" +msgstr "Jelenet" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Élő zenei videó" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Zenei videó dalszöveggel" + +msgctxt "#32209" +msgid "Concert" +msgstr "Koncert" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Kísérő film" + +msgctxt "#32211" +msgid "Short" +msgstr "Rövidfilm" + +msgctxt "#32212" +msgid "Other" +msgstr "Egyéb" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Ugrás az Albumra" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Ugrás az előadóra" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Ugrás ide {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Évad" + +msgctxt "#32304" +msgid "Episode" +msgstr "Epizód" + +msgctxt "#32305" +msgid "Extras" +msgstr "Extrák" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Kapcsolódó műsorok" + +msgctxt "#32307" +msgid "More" +msgstr "Egyéb" + +msgctxt "#32308" +msgid "Available" +msgstr "Elérhető" + +msgctxt "#32309" +msgid "None" +msgstr "Nincs" + +msgctxt "#32310" +msgid "S" +msgstr "" + +msgctxt "#32311" +msgid "E" +msgstr "" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Nem elérhető" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Ez az elem jelenleg nem érhető el." + +msgctxt "#32314" +msgid "In Progress" +msgstr "Folyamatban" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Folytatja a lejátszást?" + +msgctxt "#32316" +msgid "Resume" +msgstr "Folytat" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Lejátszás az elejétől" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Nem nézettnek jelölés" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Nézettnek jelölés" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Az évad nem nézettnek jelölése" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Az évad nézettnek jelölése" + +msgctxt "#32322" +msgid "Delete" +msgstr "Törlés" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Ugrás a műsorra" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Ugrás {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Következő lejátszása" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Tényleg törli?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Biztos benne, hogy törli az alábbi tartalmat?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Igen" + +msgctxt "#32329" +msgid "No" +msgstr "Nem" + +msgctxt "#32330" +msgid "Message" +msgstr "Üzenet" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Probléma történt a tartalom törlése közben." + +msgctxt "#32332" +msgid "Home" +msgstr "Kezdőoldal" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Lejátszási listák" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Kilépés megerősítése" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Készen áll arra, hogy kilépjen a Plexből?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Kilépés" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Mégse" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Nem található szerver" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "A szerver nem elérhető" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "A kapcsolat tesztelése folyamatban. Kérem várjon." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "A szerver nem elérhető. Kérem jelentkezzen be a szerverébe és ellenőrizze a hálózati kapcsolatot." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Felhasználóváltás" + +msgctxt "#32343" +msgid "Settings" +msgstr "Beállítások" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Kijelentkezés" + +msgctxt "#32345" +msgid "All" +msgstr "Összes" + +msgctxt "#32346" +msgid "By Name" +msgstr "Név szerint" + +msgctxt "#32347" +msgid "artists" +msgstr "előadók" + +msgctxt "#32348" +msgid "movies" +msgstr "filmek" + +msgctxt "#32349" +msgid "photos" +msgstr "fényképek" + +msgctxt "#32350" +msgid "shows" +msgstr "műsorok" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Hozzáadás dátuma szerint" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Dátum szerint" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Megjelenés dátuma szerint" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Megjelenés dátuma" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Megtekintés dátuma szerint" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Megtekintés dátuma" + +msgctxt "#32357" +msgid "By Name" +msgstr "Név szerint" + +msgctxt "#32358" +msgid "Name" +msgstr "Név" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Értékelés szerint" + +msgctxt "#32360" +msgid "Rating" +msgstr "Értékelés" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Felbontás szerint" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Felbontás" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Időtartam szerint" + +msgctxt "#32364" +msgid "Duration" +msgstr "Időtartam" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Első sugárzás dátuma szerint" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Első sugárzás dátuma" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Nem megtekintettek szerint" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Nem megtekintettek" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Lejátszás dátuma szerint" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Lejátszás dátuma" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Lájátszások száma szerint" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Lejátszások száma" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Készítés dátuma szerint" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Készítés dátuma" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Nincs elérhető szűrő" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Szűrők eltávolítása" + +msgctxt "#32377" +msgid "Year" +msgstr "Év" + +msgctxt "#32378" +msgid "Decade" +msgstr "Évtized" + +msgctxt "#32379" +msgid "Genre" +msgstr "Műfaj" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Értékelés" + +msgctxt "#32381" +msgid "Network" +msgstr "Csatorna" + +msgctxt "#32382" +msgid "Collection" +msgstr "Nyűjtemény" + +msgctxt "#32383" +msgid "Director" +msgstr "Rendező" + +msgctxt "#32384" +msgid "Actor" +msgstr "Színész" + +msgctxt "#32385" +msgid "Country" +msgstr "Ország" + +msgctxt "#32386" +msgid "Studio" +msgstr "Stúdió" + +msgctxt "#32387" +msgid "Labels" +msgstr "Cimkék" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Fényképezőgép gyártmánya" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Fényképezőgép Modell" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Rekesz" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Zár sebesség" + +msgctxt "#32392" +msgid "Lens" +msgstr "Lencsék" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Sorozatok" + +msgctxt "#32394" +msgid "Music" +msgstr "Zene" + +msgctxt "#32395" +msgid "Audio" +msgstr "Audió" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Feliratok" + +msgctxt "#32397" +msgid "Quality" +msgstr "Minőség" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Kodi videó beállitások" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Kodi audió beállitások" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Ugrás az évadra" + +msgctxt "#32401" +msgid "Directors" +msgstr "Rendezők" + +msgctxt "#32402" +msgid "Writer" +msgstr "Szerző" + +msgctxt "#32403" +msgid "Writers" +msgstr "Szerzők" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Kapcsolódó filmek" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Feliratok letöltése" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Felirat késleltetés" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Következő felirat" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Feliratok letiltása" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Feliratok engedélyezése" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Platform verzió" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Ismeretlen" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Szerkesztés vagy törlés" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Módosíthatja az IP-címet, vagy törli az aktuális beállítást?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Törlés" + +msgctxt "#32415" +msgid "Edit" +msgstr "Módositás" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Adja meg az IP-címet" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Adja meg a port számát" + +msgctxt "#32418" +msgid "Creator" +msgstr "Alkotó" + +msgctxt "#32419" +msgid "Cast" +msgstr "Szereplők" + +msgctxt "#32420" +msgid "Disc" +msgstr "Lemez" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Kijelentkezés" + +msgctxt "#32422" +msgid "Exit" +msgstr "Kilépés" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Leállítás" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Felfüggesztés" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Hibernálás" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Újraindítás" + +msgctxt "#32427" +msgid "Failed" +msgstr "Sikertelen" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Bejelentkezés sikertelen" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Folytatás innen {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Felfedezés" + +msgctxt "#32431" +msgid "Search" +msgstr "Keresés" + +msgctxt "#32432" +msgid "Space" +msgstr "Space" + +msgctxt "#32433" +msgid "Clear" +msgstr "Törlés mind" + +msgctxt "#32434" +msgid "Searching..." +msgstr "Keresés..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Nincs találat" + +msgctxt "#32436" +msgid "Paused" +msgstr "Szünetelve" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Üdvözöljük" + +msgctxt "#32438" +msgid "Previous" +msgstr "Előző" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Következő lejátszása" + +msgctxt "#32440" +msgid "On Deck" +msgstr "Jelenlegi" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Ismeretlen" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Beágyazott" + +msgctxt "#32443" +msgid "Forced" +msgstr "Kényszerített" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Dalszöveg" + +msgctxt "#32445" +msgid "Mono" +msgstr "Monó" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Sztereó" + +msgctxt "#32447" +msgid "None" +msgstr "Nincs" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "A lejátszás sikertelen!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Nem lehet csatlakozni a plex.tv-hez[CR]Ellenőrizze az internetkapcsolatot, és próbálja újra." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Verzió választása" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Verzió lejátszása..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "Nincs elérhető tartalom ebben a könyvtárban" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Kérem adjon hozzá tartalmat és/vagy ellenőrizze, hogy a 'Hozzáadás az irányítópulthoz' opció be van kapcsolva" + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Nincs elérhető tartalom ezzel a szűrővel" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Kérem változtassa meg, vagy távolítsa el a jelenlegi szűrőt" + +msgctxt "#32456" +msgid "Offline Mode" +msgstr "Offline mód" + +msgctxt "#32457" +msgid "Sign In" +msgstr "Bejelentkezés" diff --git a/script.plexmod/resources/language/resource.language.it_it/strings.po b/script.plexmod/resources/language/resource.language.it_it/strings.po new file mode 100644 index 000000000..50dee38bc --- /dev/null +++ b/script.plexmod/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,951 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2017-05-25 10:52+0200\n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: \n" +"X-Generator: Poedit 2.0.2\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Principale" + +msgctxt "#32001" +msgid "Original" +msgstr "Originale" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mbps 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mbps 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mbps 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mbps 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mbps 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mbps 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mbps 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mbps 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kbps" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kbps" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kbps" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kbps" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kbps" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Qualità Locale" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Qualità Remota" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Qualità Online" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Formato transcodifica" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Log di Debug" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Permetti Play Diretto" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Permetti Stream Diretto" + +msgctxt "#32027" +msgid "Force" +msgstr "Forza" + +msgctxt "#32028" +msgid "Always" +msgstr "Sempre" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Solo formati immagine" + +msgctxt "#32030" +msgid "Auto" +msgstr "Auto" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Sottotitoli (Solo Play Diretto)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Permetti connessioni non sicure" + +msgctxt "#32033" +msgid "Never" +msgstr "Mai" + +msgctxt "#32034" +msgid "On Same network" +msgstr "Sulla stessa rete" + +msgctxt "#32035" +msgid "Always" +msgstr "Sempre" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Permetti 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Permetti HEVC (h265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Accesso (Sign In) automatico" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Post Play Auto Play" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Abilita il download dei sottotitoli" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Abilita il download dei sottotitoli" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Ricerca Server (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Esegui Plex all'avvio di Kodi" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "IP connessione 1" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Porta connessione 1" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "IP connessione 2" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Porta connessione 2" + +msgctxt "#32048" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Avanzate" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Server manuali" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Privacy" + +msgctxt "#32052" +msgid "About" +msgstr "Circa" + +msgctxt "#32053" +msgid "Video" +msgstr "Video" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Versione Addon" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Versione Kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Risoluzione schermo" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Versione Server attuale" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "All'avvio salta l'immissione del PIN e la selezione utente." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Se abilitato, quando la riproduzione finisce e un 'Titolo Successivo' è disponibile, sarà automaticamente riprodotto dopo un ritardo di 15 secondi." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Abilita se il tuo hardware supporta la riproduzione 4K. Disabilita per forzare la trascodifica." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Abilita se il tuo hardware supporta HEVC/h265. Disabilita per forzare la trascodifica." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Quando connettersi a Server con connessioni non sicure.[CR][CR]* [B]Mai[/B]: Mai connettersi a Server non sicuri[CR]* [B]Sulla stessa Rete[/B]: Permetti se nella stessa Rete[CR]* [B]Sempre[/B]: Permetti sulla stessa Rete e su Reti remote" + +msgctxt "#32201" +msgid "Trailer" +msgstr "Trailer" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Scene cancellate" + +msgctxt "#32203" +msgid "Interview" +msgstr "Intervista" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Video musicali" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Dietro le scene" + +msgctxt "#32206" +msgid "Scene" +msgstr "Scena" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Video musicali dal vivo" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Testi video musicali" + +msgctxt "#32209" +msgid "Concert" +msgstr "Concerto" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Featurette" + +msgctxt "#32211" +msgid "Short" +msgstr "Corto" + +msgctxt "#32212" +msgid "Other" +msgstr "Altro" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Vai all'Album" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Vai all'Artista" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Vai a {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Stagione" + +msgctxt "#32304" +msgid "Episode" +msgstr "Episodio" + +msgctxt "#32305" +msgid "Extras" +msgstr "Extras" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Serie correlate" + +msgctxt "#32307" +msgid "More" +msgstr "Più" + +msgctxt "#32308" +msgid "Available" +msgstr "Disponibile" + +msgctxt "#32309" +msgid "None" +msgstr "Nessuno" + +msgctxt "#32310" +msgid "S" +msgstr "S" + +msgctxt "#32311" +msgid "E" +msgstr "E" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Non disponibile" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Questo elemento non è al momento diponibile" + +msgctxt "#32314" +msgid "In Progress" +msgstr "In Corso" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Riprendere la riproduzione?" + +msgctxt "#32316" +msgid "Resume" +msgstr "Riprendi" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Riproduci dall'inizio" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Marca come non visto" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Marca come visto" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Marca la Stagione come non vista" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Marca la Stagione come vista" + +msgctxt "#32322" +msgid "Delete" +msgstr "Cancellato" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Vai alla Serie" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Vai a {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Riproduci il titolo succesivo" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Cancellare veramente?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Sei sucuro che vuoi davvero cancellare questo media?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Sì" + +msgctxt "#32329" +msgid "No" +msgstr "No" + +msgctxt "#32330" +msgid "Message" +msgstr "Messaggio" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "C'è stato un problema nella cancellazione del media." + +msgctxt "#32332" +msgid "Home" +msgstr "Home" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Playlists" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Conferma Uscita" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Sei pronto per uscire da Plex?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Esci" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Cancella" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Nessun Server trovato" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Server non accessibile" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Test di connessione in corso. Per favore attenti." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Server non accessibile. Per favore fai il sign in nel Server a controlla la connessione." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Cambia utente" + +msgctxt "#32343" +msgid "Settings" +msgstr "Impostazioni" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Esci (Sign Out)" + +msgctxt "#32345" +msgid "All" +msgstr "Tutto" + +msgctxt "#32346" +msgid "By Name" +msgstr "Per Nome" + +msgctxt "#32347" +msgid "Artists" +msgstr "Artisti" + +msgctxt "#32348" +msgid "movies" +msgstr "film" + +msgctxt "#32349" +msgid "photos" +msgstr "foto" + +msgctxt "#32350" +msgid "Shows" +msgstr "Serie" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Per data di aggiunta" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Data di aggiunta" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Per data di uscita" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Data di uscita" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Per data di già visto" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Data di già visto" + +msgctxt "#32357" +msgid "By Name" +msgstr "Per nome" + +msgctxt "#32358" +msgid "Name" +msgstr "Nome" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Per Valutazione" + +msgctxt "#32360" +msgid "Rating" +msgstr "Valutazione" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Per Risoluzione" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Risoluzione" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Per Durata" + +msgctxt "#32364" +msgid "Duration" +msgstr "Durata" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Per prima trasmissione" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Prima trasmissione" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Per Non visto" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Non visto" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Per data di riproduzione" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Data di riproduzione" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Per numero di volte riprodotto" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Numero di volte riprodotto" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Per data di ripresa" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Data di ripresa" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Nussun filtro disponibile" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Pulisci i filtri" + +msgctxt "#32377" +msgid "Year" +msgstr "Anno" + +msgctxt "#32378" +msgid "Decade" +msgstr "Decennio" + +msgctxt "#32379" +msgid "Genre" +msgstr "Genere" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Classificazione dei contenuti" + +msgctxt "#32381" +msgid "Network" +msgstr "Rete" + +msgctxt "#32382" +msgid "Collection" +msgstr "Collezione" + +msgctxt "#32383" +msgid "Director" +msgstr "Regista" + +msgctxt "#32384" +msgid "Actor" +msgstr "Attore" + +msgctxt "#32385" +msgid "Country" +msgstr "Nazione" + +msgctxt "#32386" +msgid "Studio" +msgstr "Studio" + +msgctxt "#32387" +msgid "Labels" +msgstr "Etichette" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Marca camera" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Modello camera" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Apertura" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Velocità di scatto" + +msgctxt "#32392" +msgid "Lens" +msgstr "Lenti" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Serie televisive" + +msgctxt "#32394" +msgid "Music" +msgstr "Musica" + +msgctxt "#32395" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Sottotitoli" + +msgctxt "#32397" +msgid "Quality" +msgstr "Qualità" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Impostazioni Video di Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Impostazioni Audio di Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Vai alla Stagione" + +msgctxt "#32401" +msgid "Directors" +msgstr "Registi" + +msgctxt "#32402" +msgid "Writer" +msgstr "Autore" + +msgctxt "#32403" +msgid "Writers" +msgstr "Autori" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Film correlati" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Scarica Sottotitoli" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Ritardo Sottotitolo" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Sottotitolo successivo" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Disabilita sottotitoli" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Abilita Sottotitoli" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Versione piattaforma" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Sconosciuto" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Modifica o Cancella" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Modificare l'indirizzo IP o cancellare le impostazioni correnti?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Cancella" + +msgctxt "#32415" +msgid "Edit" +msgstr "Modifica" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Inserisci l'indirizzo IP" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Inserisci il numero della Porta" + +msgctxt "#32418" +msgid "Creator" +msgstr "Creatore" + +msgctxt "#32419" +msgid "Cast" +msgstr "Cast" + +msgctxt "#32420" +msgid "Disc" +msgstr "Disc" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Esci (Sign Out)" + +msgctxt "#32422" +msgid "Exit" +msgstr "Esci" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Spegni" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Sospenti" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Iberna" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Riavvia" + +msgctxt "#32427" +msgid "Failed" +msgstr "Fallito" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Login fallita!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Riprendi da {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Ricerca" + +msgctxt "#32431" +msgid "Search" +msgstr "Cerca" + +msgctxt "#32432" +msgid "Space" +msgstr "Spazio" + +msgctxt "#32433" +msgid "Clear" +msgstr "Pulisci" + +msgctxt "#32434" +msgid "Searching..." +msgstr "In ricerca..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Nessun risultato" + +msgctxt "#32436" +msgid "Paused" +msgstr "In Pausa" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Benvenuto" + +msgctxt "#32438" +msgid "Previous" +msgstr "Precedente" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Titolo successivo" + +msgctxt "#32440" +msgid "On Deck" +msgstr "Scoprire" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Sconosciuto" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Integrato" + +msgctxt "#32443" +msgid "Forced" +msgstr "Forzato" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Testi" + +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Stereo" + +msgctxt "#32447" +msgid "None" +msgstr "Nessuno" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Riproduzione fallita!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Impossibile connettersi a plex.tv[CR]Controlla la tua connessione ad Internet e riprova." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Seleziona Versione" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Versione in Play..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "Nessun contenuto disponibile in questa libreria" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Per favore aggiungi contenuti e/o controlla che 'Includi nella dashboard' è abilitato." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Nessun contenuto disponibile per questo filtro" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Per favore cambia o rimuovi il filtro corrente" + +msgctxt "#32456" +msgid "Show" +msgstr "Serie" + +msgctxt "#32457" +msgid "By Show" +msgstr "Per Serie" + +msgctxt "#32458" +msgid "Episodes" +msgstr "Episodi" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Modo Offline" + +msgctxt "#32460" +msgid "Sign In" +msgstr "Accedi (Sign In)" + +msgctxt "#32461" +msgid "Albums" +msgstr "Albums" + +msgctxt "#32462" +msgid "Artist" +msgstr "Artista" + +msgctxt "#32463" +msgid "By Artist" +msgstr "Per Artista" diff --git a/script.plexmod/resources/language/resource.language.pl_pl/strings.po b/script.plexmod/resources/language/resource.language.pl_pl/strings.po new file mode 100644 index 000000000..d8b263ae0 --- /dev/null +++ b/script.plexmod/resources/language/resource.language.pl_pl/strings.po @@ -0,0 +1,984 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2020-06-25 12:04+0200\n" +"Last-Translator: Lukasz Zacharski \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Główne" + +msgctxt "#32001" +msgid "Original" +msgstr "Oryginalna" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mb/s 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mb/s 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mb/s 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mb/s 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mb/s 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mb/s 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mb/s 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mb/s 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kb/s" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kb/s" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kb/s" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kb/s" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kb/s" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Jakość lokalna" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Jakość zdalna" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Jakość online" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Format transkodowania" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Dziennik debugowania" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Zezwól na bezpośrednie odtwarzanie" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Zezwól na bezpośrednie strumieniowanie" + +msgctxt "#32027" +msgid "Force" +msgstr "Wymuszaj" + +msgctxt "#32028" +msgid "Always" +msgstr "Zawsze" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Tylko formaty graficzne" + +msgctxt "#32030" +msgid "Auto" +msgstr "Automatyczne" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Wypalaj napisy (tylko bezpośrednie odtwarzanie)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Zezwól na niezabezpieczone połączenia" + +msgctxt "#32033" +msgid "Never" +msgstr "Nigdy" + +msgctxt "#32034" +msgid "On Same network" +msgstr "W tej samej sieci" + +msgctxt "#32035" +msgid "Always" +msgstr "Zawsze" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Zezwól na 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Zezwól na HEVC (H.265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Zaloguj się automatycznie" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Odtwarzaj „Następne” automatycznie" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Włącz pobieranie napisów" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Włącz pobieranie napisów" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Odnajdowanie serwera (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Uruchom Plex przy starcie Kodi" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "IP połączenia 1" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Port połączenia 1" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "IP połączenia 2" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Port połączenia 2" + +msgctxt "#32048" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Zaawansowane" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Ręcznie dodane serwery" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Prywatność" + +msgctxt "#32052" +msgid "About" +msgstr "O dodatku" + +msgctxt "#32053" +msgid "Video" +msgstr "Wideo" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Wersja dodatku" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Wersja Kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Rozdzielczość ekranu" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Obecna wersja serwera" + + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Pomiń wybór użytkownika i kod PIN przy starcie." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Jeśli włączone, po zakończeniu odtwarzania i dostępnej pozycji w „Następne”, zostanie ona automatycznie odtworzona po 15 sekundach." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Włącz, jeśli Twój sprzęt obsługuje odtwarzanie 4K. Wyłącz, aby wymusić transkodowanie." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Włącz, jeśli Twój sprzęt obsługuje HEVC/H.265. Wyłącz, aby wymusić transkodowanie." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Kiedy łączyć z serwerami bez zabezpieczonych połączeń.[CR][CR]* [B]Nigdy[/B]: Nigdy nie łącz w sposób niezabezpieczony[CR]* [B]W tej samej sieci[/B]: Zezwól, jeśli w tej samej sieci[CR]* [B]Zawsze[/B]: Zezwól w tej samej sieci i zdalnie" + + +msgctxt "#32201" +msgid "Trailer" +msgstr "Zwiastun" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Usunięta scena" + +msgctxt "#32203" +msgid "Interview" +msgstr "Wywiad" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Teledysk" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Za kulisami" + +msgctxt "#32206" +msgid "Scene" +msgstr "Scena" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Teledysk na żywo" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Teledysk z tekstem" + +msgctxt "#32209" +msgid "Concert" +msgstr "Koncert" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Średniometrażowy" + +msgctxt "#32211" +msgid "Short" +msgstr "Krótkometrażowy" + +msgctxt "#32212" +msgid "Other" +msgstr "Inne" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Przejdź do albumu" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Przejdź do wykonawcy" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Przejdź do {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Sezon" + +msgctxt "#32304" +msgid "Episode" +msgstr "Odcinek" + +msgctxt "#32305" +msgid "Extras" +msgstr "Dodatki" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Powiązane seriale" + +msgctxt "#32307" +msgid "More" +msgstr "Więcej" + +msgctxt "#32308" +msgid "Available" +msgstr "Dostępne" + +msgctxt "#32309" +msgid "None" +msgstr "Brak" + +msgctxt "#32310" +msgid "S" +msgstr "S" + +msgctxt "#32311" +msgid "E" +msgstr "O" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Niedostępne" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Ten element jest obecnie niedostępny." + +msgctxt "#32314" +msgid "In Progress" +msgstr "W toku" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Wznowić odtwarzanie" + +msgctxt "#32316" +msgid "Resume" +msgstr "Wznów" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Odtwórz od początku" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Oznacz jako nieodtworzone" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Oznacz jako odtworzone" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Oznacz sezon jako nieodtworzony" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Oznacz sezon jako odtworzony" + +msgctxt "#32322" +msgid "Delete" +msgstr "Usuń" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Przejdź do serialu" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Przejdź do {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Odtwórz następne" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Na pewno usunąć?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Czy na pewno chcesz usunąć te multimedia?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Tak" + +msgctxt "#32329" +msgid "No" +msgstr "Nie" + +msgctxt "#32330" +msgid "Message" +msgstr "Wiadomość" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Wystąpił problem z usunięciem multimediów." + +msgctxt "#32332" +msgid "Home" +msgstr "Strona główna" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Playlisty" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Potwierdź wyjście" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Czy chcesz wyjść z Plex?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Wyjdź" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Anuluj" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Nie znaleziono serwerów" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Serwer jest niedostępny" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Trwa testowanie połączeń. Proszę czekać." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Serwer jest niedostępny. Zaloguj się na swój serwer i sprawdź połączenie." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Przełącz użytkownika" + +msgctxt "#32343" +msgid "Settings" +msgstr "Ustawienia" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Wyloguj się" + +msgctxt "#32345" +msgid "All" +msgstr "Wszystkie" + +msgctxt "#32346" +msgid "By Name" +msgstr "Wg nazwy" + +msgctxt "#32347" +msgid "Artists" +msgstr "Wykonawcy" + +msgctxt "#32348" +msgid "movies" +msgstr "Filmy" + +msgctxt "#32349" +msgid "photos" +msgstr "Zdjęcia" + +msgctxt "#32350" +msgid "Shows" +msgstr "Seriale" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Wg daty dodania" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Data dodania" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Wg daty wydania" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Data wydania" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Wg daty wyświetlenia" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Data wyświetlenia" + +msgctxt "#32357" +msgid "By Name" +msgstr "Wg nazwy" + +msgctxt "#32358" +msgid "Name" +msgstr "Nazwa" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Wg oceny" + +msgctxt "#32360" +msgid "Rating" +msgstr "Ocena" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Wg rozdzielczości" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Rozdzielczość" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Wg czasu trwania" + +msgctxt "#32364" +msgid "Duration" +msgstr "Czas trwania" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Wg pierwszej emisji" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Pierwsza emisja" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Wg nieodtworzonych" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Nieodtworzone" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Wg daty odtworzenia" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Data odtworzenia" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Wg ilości odtworzeń" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Ilość odtworzeń" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Wg daty wykonania" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Data wykonania" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Brak dostępnych filtrów" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Wyczyść filtr" + +msgctxt "#32377" +msgid "Year" +msgstr "Rok" + +msgctxt "#32378" +msgid "Decade" +msgstr "Dekada" + +msgctxt "#32379" +msgid "Genre" +msgstr "Gatunek" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Ocena treści" + +msgctxt "#32381" +msgid "Network" +msgstr "Stacja" + +msgctxt "#32382" +msgid "Collection" +msgstr "Kolekcja" + +msgctxt "#32383" +msgid "Director" +msgstr "Reżyser" + +msgctxt "#32384" +msgid "Actor" +msgstr "Aktor" + +msgctxt "#32385" +msgid "Country" +msgstr "Kraj" + +msgctxt "#32386" +msgid "Studio" +msgstr "Studio" + +msgctxt "#32387" +msgid "Labels" +msgstr "Etykiety" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Producent aparatu" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Model aparatu" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Przysłona" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Czas migawki" + +msgctxt "#32392" +msgid "Lens" +msgstr "Obiektyw" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Seriale" + +msgctxt "#32394" +msgid "Music" +msgstr "Muzyka" + +msgctxt "#32395" +msgid "Audio" +msgstr "Audio" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Napisy" + +msgctxt "#32397" +msgid "Quality" +msgstr "Jakość" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Ustawienia wideo Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Ustawienia audio Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Przejdź do sezonu" + +msgctxt "#32401" +msgid "Directors" +msgstr "Reżyserzy" + +msgctxt "#32402" +msgid "Writer" +msgstr "Scenarzysta" + +msgctxt "#32403" +msgid "Writers" +msgstr "Scenarzyści" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Powiązane filmy" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Pobierz napisy" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Opóźnienie napisów" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Następne napisy" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Wyłącz napisy" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Włącz napisy" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Wersja platformy" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Nieznana" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Edytuj lub wyczyść" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Edytować adres IP lub wyczyścić obecne ustawienie?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Wyczyść" + +msgctxt "#32415" +msgid "Edit" +msgstr "Edytuj" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Wprowadź adres IP" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Wprowadź numer portu" + +msgctxt "#32418" +msgid "Creator" +msgstr "Twórca" + +msgctxt "#32419" +msgid "Cast" +msgstr "Obsada" + +msgctxt "#32420" +msgid "Disc" +msgstr "Dysk" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Wyloguj się" + +msgctxt "#32422" +msgid "Exit" +msgstr "Wyjdź" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Zamknij" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Uśpij" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Hibernuj" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Uruchom ponownie" + +msgctxt "#32427" +msgid "Failed" +msgstr "Nieudane" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Logowanie nieudane!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Wznów od {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Odkrywanie" + +msgctxt "#32431" +msgid "Search" +msgstr "Wyszukiwanie" + +msgctxt "#32432" +msgid "Space" +msgstr "Spacja" + +msgctxt "#32433" +msgid "Clear" +msgstr "Wyczyść" + +msgctxt "#32434" +msgid "Searching..." +msgstr "Wyszukiwanie..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Brak wyników" + +msgctxt "#32436" +msgid "Paused" +msgstr "Wstrzymano" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Witaj" + +msgctxt "#32438" +msgid "Previous" +msgstr "Poprzednie" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Odtwarzanie następnego" + +msgctxt "#32440" +msgid "On Deck" +msgstr "Na tapecie" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Nieznany" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Osadzone" + +msgctxt "#32443" +msgid "Forced" +msgstr "Wymuszone" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Tekst" + +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Stereo" + +msgctxt "#32447" +msgid "None" +msgstr "Brak" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Odtwarzanie nieudane!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Nie można połączyć z plex.tv[CR]Sprawdź połączenie internetowe i spróbuj ponownie." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Wybierz wersję" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Odtwórz wersję..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "Brak zawartości w tej bibliotece" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Dodaj zawartość i/lub sprawdź, czy opcja „Załącz na pulpicie” jest włączona." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Brak zawartości dla tego filtra" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Zmień lub usuń bieżący filtr" + +msgctxt "#32456" +msgid "Show" +msgstr "Serial" + +msgctxt "#32457" +msgid "By Show" +msgstr "Wg serialu" + +msgctxt "#32458" +msgid "Episodes" +msgstr "Odcinki(ów)" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Tryb offline" + +msgctxt "#32460" +msgid "Sign In" +msgstr "Zaloguj się" + +msgctxt "#32461" +msgid "Albums" +msgstr "Albumy" + +msgctxt "#32462" +msgid "Artist" +msgstr "Wykonawca" + +msgctxt "#32463" +msgid "By Artist" +msgstr "Wg wykonawcy" + +msgctxt "#32464" +msgid "Player" +msgstr "Odtwarzacz" + +msgctxt "#32465" +msgid "Use skip step settings from Kodi" +msgstr "Użyj ustawień kroków przeskoku z Kodi" + +msgctxt "#32466" +msgid "Automatically seek selected position after a delay" +msgstr "Automatycznie szukaj wybranej pozycji po opóźnieniu" + +msgctxt "#32467" +msgid "User Interface" +msgstr "Interfejs użytkownika" + +msgctxt "#32471" +msgid "Use Plex/Kodi steps for timeline" +msgstr "Użyj kroków Plex/Kodi dla osi czasu" + +msgctxt "#32485" +msgid "Go back instantly with the previous menu action in scrolled views" +msgstr "Wróć natychmiast akcją menu „poprzednie” w przewijanych widokach" + +msgctxt "#32492" +msgid "Kodi Subtitle Settings" +msgstr "Ustawienia napisów Kodi" + +msgctxt "#32495" +msgid "Skip intro" +msgstr "Pomiń wstęp" diff --git a/script.plexmod/resources/language/resource.language.pt_br/strings.po b/script.plexmod/resources/language/resource.language.pt_br/strings.po new file mode 100644 index 000000000..98bd80061 --- /dev/null +++ b/script.plexmod/resources/language/resource.language.pt_br/strings.po @@ -0,0 +1,951 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt-BR\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Principal" + +msgctxt "#32001" +msgid "Original" +msgstr "Original" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mbps 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mbps 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mbps 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mbps 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mbps 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mbps 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mbps 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mbps 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kbps" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kbps" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kbps" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kbps" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kbps" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Qualidade Local" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Qualidade Remota" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Qualidade Online" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Formato de Transcodificação" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Registro de Depuração" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Permitir Reprodução Direta" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Permitir Transmissão Direta" + +msgctxt "#32027" +msgid "Force" +msgstr "Forçar" + +msgctxt "#32028" +msgid "Always" +msgstr "Sempre" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Apenas Formatos de Imagem" + +msgctxt "#32030" +msgid "Auto" +msgstr "Auto" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Gravar Legendas (Somente Reprodução Direta)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Permitir Conexões Inseguras" + +msgctxt "#32033" +msgid "Never" +msgstr "Nunca" + +msgctxt "#32034" +msgid "On Same network" +msgstr "Na Mesma Rede" + +msgctxt "#32035" +msgid "Always" +msgstr "Sempre" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Permitir 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Permitir HEVC (h265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Conectar Automaticamente" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Reprodução Automática Após Reprodução" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Habilitar Download de Legendas" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Habilitar Download de Legendas" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Descoberta de Servidor (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Iniciar Plex quando Kodi Iniciar" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "Conexão de IP 1" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Conexão de Porta 1" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "Conexão de IP 2" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Conexão de Porta 2" + +msgctxt "#32048" +msgid "Audio" +msgstr "Áudio" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Avançado" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Servidores Manuais" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Privacidade" + +msgctxt "#32052" +msgid "About" +msgstr "Sobre" + +msgctxt "#32053" +msgid "Video" +msgstr "Vídeo" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Versão do Addon" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Versão do Kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Resolução da Tela" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Versão Atual do Servidor" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Pular seleção de usuário e inserção do PIN ao iniciar." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Se habilitado, quando a reprodução acabar e existir algum item 'A Seguir', será automáticamente reproduzido depois de 15 segudos." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Habilite isto se seu equipamento suporta reprodução 4K. Desabilite para forçar transcodificação." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Habilite isto se seu equipamento suporta HEVC/h265. Desabilite para forçar transcodificação." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Quando conectar à servidores sem conexão segura.[CR][CR]* [B]Nunca[/B]: Nunca conectar à um servidor inseguramente[CR]* [B]Na Mesma Rede[/B]: Permitir se na mesma reder[CR]* [B]Sempre[/B]: Permitir conexão na mesma rede e remotamente" + +msgctxt "#32201" +msgid "Trailer" +msgstr "Trailer" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Cena apagada" + +msgctxt "#32203" +msgid "Interview" +msgstr "Entrevista" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Videoclipe" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Por trás das cenas" + +msgctxt "#32206" +msgid "Scene" +msgstr "Cena" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Videoclipe ao vivo" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Videclipe com Letra" + +msgctxt "#32209" +msgid "Concert" +msgstr "Show" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Featurette" + +msgctxt "#32211" +msgid "Short" +msgstr "Curta" + +msgctxt "#32212" +msgid "Other" +msgstr "Outro" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Ir para Álbum" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Ir para Artista" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Ir para {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Temporada" + +msgctxt "#32304" +msgid "Episode" +msgstr "Episódio" + +msgctxt "#32305" +msgid "Extras" +msgstr "Extras" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Séries relacionadas" + +msgctxt "#32307" +msgid "More" +msgstr "Mais" + +msgctxt "#32308" +msgid "Available" +msgstr "Disponível" + +msgctxt "#32309" +msgid "None" +msgstr "Nenhum" + +msgctxt "#32310" +msgid "S" +msgstr "S" + +msgctxt "#32311" +msgid "E" +msgstr "E" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Indisponível" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Este item está atualmente indisponível." + +msgctxt "#32314" +msgid "In Progress" +msgstr "Em Progresso" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Resumir reprodução" + +msgctxt "#32316" +msgid "Resume" +msgstr "Resumir" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Reproduzir do começo" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Marcar como Não-Visto" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Marcar como Visto" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Marcar Temporada como Não-Vista" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Marcar Temporada como Vista" + +msgctxt "#32322" +msgid "Delete" +msgstr "Apagar" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Ir para Série" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Ir para {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Reproduzir Próximo" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Realmente apagar?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Tem certeza que quer apagar esta mídia?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Sim" + +msgctxt "#32329" +msgid "No" +msgstr "Não" + +msgctxt "#32330" +msgid "Message" +msgstr "Mensagem" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Ocorreu um problema ao apagar esta mídia." + +msgctxt "#32332" +msgid "Home" +msgstr "Início" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Listas de Reprodução" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Confirmar Saída" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Está pronto para sair do Plex?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Sair" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Cancelar" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Nenhum Servidor Encontrado" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Servidor inacessível" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Testes de conexão estão em progresso. Por favor, aguarde." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Servidor inacessível. Conecte em seu servidor e teste sua conexão." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Trocar Usuário" + +msgctxt "#32343" +msgid "Settings" +msgstr "Configurações" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Desconectar" + +msgctxt "#32345" +msgid "All" +msgstr "Tudo" + +msgctxt "#32346" +msgid "By Name" +msgstr "Por Nome" + +msgctxt "#32347" +msgid "Artists" +msgstr "Artistas" + +msgctxt "#32348" +msgid "movies" +msgstr "filmes" + +msgctxt "#32349" +msgid "photos" +msgstr "fotos" + +msgctxt "#32350" +msgid "Shows" +msgstr "Séries" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Por Data Adicionado" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Data Adicionado" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Por Data de Lançamento" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Data de Lançamento" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Por Data de Visualização" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Data de Visualização" + +msgctxt "#32357" +msgid "By Name" +msgstr "Por Nome" + +msgctxt "#32358" +msgid "Name" +msgstr "Nome" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Por Avaliação" + +msgctxt "#32360" +msgid "Rating" +msgstr "Avaliação" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Por Resolução" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Resolução" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Por Duração" + +msgctxt "#32364" +msgid "Duration" +msgstr "Duração" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Por Data de Estreia" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Data de Estreia" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Por Não-Vistos" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Não-Vistos" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Por Data de Reprodução" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Data de Reprodução" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Por Número de Reproduções" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Número de Reproduções" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Por Data Tirada" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Data Tirada" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Nenhum filtro disponível" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Limpar filtro" + +msgctxt "#32377" +msgid "Year" +msgstr "Ano" + +msgctxt "#32378" +msgid "Decade" +msgstr "Década" + +msgctxt "#32379" +msgid "Genre" +msgstr "Gênero" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Classificação do Conteúdo" + +msgctxt "#32381" +msgid "Network" +msgstr "Rede" + +msgctxt "#32382" +msgid "Collection" +msgstr "Coleção" + +msgctxt "#32383" +msgid "Director" +msgstr "Diretor" + +msgctxt "#32384" +msgid "Actor" +msgstr "Ator" + +msgctxt "#32385" +msgid "Country" +msgstr "País" + +msgctxt "#32386" +msgid "Studio" +msgstr "Estúdio" + +msgctxt "#32387" +msgid "Labels" +msgstr "Rótulos" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Fabricante da câmera" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Modelo da câmera" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Abertura" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Velocidade do Obturador" + +msgctxt "#32392" +msgid "Lens" +msgstr "Lente" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Séries" + +msgctxt "#32394" +msgid "Music" +msgstr "Música" + +msgctxt "#32395" +msgid "Audio" +msgstr "Áudio" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Legendas" + +msgctxt "#32397" +msgid "Quality" +msgstr "Qualidade" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Configurações de Vídeo do Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Configurações de Áudio do Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Ir para Temporada" + +msgctxt "#32401" +msgid "Directors" +msgstr "Diretores" + +msgctxt "#32402" +msgid "Writer" +msgstr "Escritor" + +msgctxt "#32403" +msgid "Writers" +msgstr "Escritores" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Filmes relacionados" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Baixar Legendas" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Atraso da Legenda" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Próxima Legenda" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Desabilitar Legendas" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Habilitar Legendas" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Versão da Plataforma" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Desconhecido" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Editar ou Limpar" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Editar endereço de IP ou limpar configurações atuais?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Limpar" + +msgctxt "#32415" +msgid "Edit" +msgstr "Editar" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Insira o endereço de IP" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Insira o número da Porta" + +msgctxt "#32418" +msgid "Creator" +msgstr "Criador" + +msgctxt "#32419" +msgid "Cast" +msgstr "Elenco" + +msgctxt "#32420" +msgid "Disc" +msgstr "Disco" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Desconectar" + +msgctxt "#32422" +msgid "Exit" +msgstr "Sair" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Desligar" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Suspender" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Hibernar" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Reiniciar" + +msgctxt "#32427" +msgid "Failed" +msgstr "Falhou" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Login falhou!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Resumir de {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Descobrir" + +msgctxt "#32431" +msgid "Search" +msgstr "Procurar" + +msgctxt "#32432" +msgid "Space" +msgstr "Espaço" + +msgctxt "#32433" +msgid "Clear" +msgstr "Limpar" + +msgctxt "#32434" +msgid "Searching..." +msgstr "Procurando..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Sem Resultados" + +msgctxt "#32436" +msgid "Paused" +msgstr "Pausado" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Bem-Vindo" + +msgctxt "#32438" +msgid "Previous" +msgstr "Anterior" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Reproduzindo a seguir" + +msgctxt "#32440" +msgid "On Deck" +msgstr "No Deck" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Desconhecido" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Embutido" + +msgctxt "#32443" +msgid "Forced" +msgstr "Forçado" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Letra da música" + +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Estéreo" + +msgctxt "#32447" +msgid "None" +msgstr "Nenhum" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Reprodução falhou!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Nao pôde conectar com o plex.tv[CR]Teste sua conexão com a internet e tente novamente." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Escolha a Versão" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Reproduzir Versão..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "Nenhum conteúdo disponível nesta biblioteca" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Por favor adicione e/ou confirme que 'Incluir no Painel' está habilitado." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Nenhum conteúdo disponível para este filtro" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Por favor, troque ou remova o filtro atual" + +msgctxt "#32456" +msgid "Show" +msgstr "Série" + +msgctxt "#32457" +msgid "By Show" +msgstr "Por Série" + +msgctxt "#32458" +msgid "Episodes" +msgstr "Episódios" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Modo Offline" + +msgctxt "#32460" +msgid "Sign In" +msgstr "Conectar" + +msgctxt "#32461" +msgid "Albums" +msgstr "Álbuns" + +msgctxt "#32462" +msgid "Artist" +msgstr "Artista" + +msgctxt "#32463" +msgid "By Artist" +msgstr "Por Artista" + diff --git a/script.plexmod/resources/language/resource.language.pt_pt/strings.po b/script.plexmod/resources/language/resource.language.pt_pt/strings.po new file mode 100644 index 000000000..7c265ff3c --- /dev/null +++ b/script.plexmod/resources/language/resource.language.pt_pt/strings.po @@ -0,0 +1,980 @@ +# XBMC Media Center language file +# Guerreiro , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2019-09-10 18:44+0100\n" +"Last-Translator: Guerreiro \n" +"Language-Team: Português <>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Principal" + +msgctxt "#32001" +msgid "Original" +msgstr "Original" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Mbps 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Mbps 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Mbps 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Mbps 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Mbps 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Mbps 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Mbps 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Mbps 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 kbps" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 kbps" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 kbps" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 kbps" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 kbps" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Qualidade Local" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Qualidade Remota" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Qualidade Online" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Formato de Transcodificação" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Registo de Depuração" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Permitir Reprodução Direta" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Permitir Transmissão Direta" + +msgctxt "#32027" +msgid "Force" +msgstr "Forçar" + +msgctxt "#32028" +msgid "Always" +msgstr "Sempre" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Apenas Formatos de Imagem" + +msgctxt "#32030" +msgid "Auto" +msgstr "Auto" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Gravar Legendas (Somente Reprodução Direta)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Permitir Ligações Inseguras" + +msgctxt "#32033" +msgid "Never" +msgstr "Nunca" + +msgctxt "#32034" +msgid "On Same network" +msgstr "Na Mesma Rede" + +msgctxt "#32035" +msgid "Always" +msgstr "Sempre" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Permitir 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Permitir HEVC (h265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Entrar Automaticamente" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Reprodução Automática" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Ativar a Transferência de Legendas" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Ativar a Transferência de Legendas" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Descoberta de Servidor (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Iniciar Plex Automaticamente" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "IP de Ligação 1" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Porta de Ligação 1" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "IP de Ligação 2" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Porta de Ligação 2" + +msgctxt "#32048" +msgid "Audio" +msgstr "Áudio" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Avançado" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Servidores Manuais" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Privacidade" + +msgctxt "#32052" +msgid "About" +msgstr "Sobre" + +msgctxt "#32053" +msgid "Video" +msgstr "Vídeo" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Versão do Addon" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Versão do Kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Resolução de Ecrã" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Versão Atual do Servidor" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Saltar seleção de utilizador e inserção do PIN ao iniciar." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Se ativado, quando a reprodução acabar e existir algum item 'A Seguir', será automáticamente reproduzido depois de 15 segundos." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Ative isto se seu equipamento suportar reprodução 4K. Desative para forçar conversão." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Ative isto se seu equipamento suportar HEVC/h265. Desative para forçar conversão." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Quando ligar à servidores sem ligação segura.[CR][CR]* [B]Nunca[/B]: Nunca ligar à um servidor de forma insegura[CR]* [B]Na Mesma Rede[/B]: Permitir se na mesma rede[CR]* [B]Sempre[/B]: Permitir ligação na mesma rede e remotamente" + +msgctxt "#32201" +msgid "Trailer" +msgstr "Trailer" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Cena apagada" + +msgctxt "#32203" +msgid "Interview" +msgstr "Entrevista" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Vídeo Clipe" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "Nos Bastidores" + +msgctxt "#32206" +msgid "Scene" +msgstr "Cena" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Vídeo Clipe ao Vivo" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Vide Clipe com Letra" + +msgctxt "#32209" +msgid "Concert" +msgstr "Concerto" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Destaques" + +msgctxt "#32211" +msgid "Short" +msgstr "Curta" + +msgctxt "#32212" +msgid "Other" +msgstr "Outro" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Ir para Álbum" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Ir para Artista" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Ir para {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Temporada" + +msgctxt "#32304" +msgid "Episode" +msgstr "Episódio" + +msgctxt "#32305" +msgid "Extras" +msgstr "Extras" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Séries relacionadas" + +msgctxt "#32307" +msgid "More" +msgstr "Mais" + +msgctxt "#32308" +msgid "Available" +msgstr "Disponível" + +msgctxt "#32309" +msgid "None" +msgstr "Nenhum" + +msgctxt "#32310" +msgid "S" +msgstr "T" + +msgctxt "#32311" +msgid "E" +msgstr "E" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Indisponível" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Este item está atualmente indisponível." + +msgctxt "#32314" +msgid "In Progress" +msgstr "Em Progresso" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Resumir reprodução" + +msgctxt "#32316" +msgid "Resume" +msgstr "Resumir" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Reproduzir do inicio" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Marcar como Não-Visto" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Marcar como Visto" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Marcar Temporada como Não-Vista" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Marcar Temporada como Vista" + +msgctxt "#32322" +msgid "Delete" +msgstr "Apagar" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Ir para Série" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Ir para {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Reproduzir Próximo" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Realmente Apagar?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Tem certeza que quer apagar este item?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Sim" + +msgctxt "#32329" +msgid "No" +msgstr "Não" + +msgctxt "#32330" +msgid "Message" +msgstr "Mensagem" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Ocorreu um problema ao apagar este item." + +msgctxt "#32332" +msgid "Home" +msgstr "Início" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Listas de Reprodução" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Confirmar Saída" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Está pronto para sair do Plex?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Sair" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Cancelar" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Nenhum Servidor Encontrado" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Servidor inacessível" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Testes de ligação estão em progresso. Por favor, aguarde." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Servidor inacessível. Ligue-se ao servidor e teste a ligação." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Mudar de Utilizador" + +msgctxt "#32343" +msgid "Settings" +msgstr "Definições" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Terminar Sessão" + +msgctxt "#32345" +msgid "All" +msgstr "Tudo" + +msgctxt "#32346" +msgid "By Name" +msgstr "Por Nome" + +msgctxt "#32347" +msgid "Artists" +msgstr "Artistas" + +msgctxt "#32348" +msgid "Movies" +msgstr "Filmes" + +msgctxt "#32349" +msgid "photos" +msgstr "fotos" + +msgctxt "#32350" +msgid "Shows" +msgstr "Séries" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "Por Data Adicionado" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Data Adicionado" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "Por Data de Lançamento" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Data de Lançamento" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "Por Data de Visualização" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Data de Visualização" + +msgctxt "#32357" +msgid "By Name" +msgstr "Por Nome" + +msgctxt "#32358" +msgid "Name" +msgstr "Nome" + +msgctxt "#32359" +msgid "By Rating" +msgstr "Por Avaliação" + +msgctxt "#32360" +msgid "Rating" +msgstr "Avaliação" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "Por Resolução" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Resolução" + +msgctxt "#32363" +msgid "By Duration" +msgstr "Por Duração" + +msgctxt "#32364" +msgid "Duration" +msgstr "Duração" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "Por Data de Estreia" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Data de Estreia" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "Por Não-Vistos" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Não-Vistos" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "Por Data de Reprodução" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Data de Reprodução" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "Por Número de Reproduções" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Número de Reproduções" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "Por Data Tirada" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Data Tirada" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Nenhum filtro disponível" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Limpar filtro" + +msgctxt "#32377" +msgid "Year" +msgstr "Ano" + +msgctxt "#32378" +msgid "Decade" +msgstr "Década" + +msgctxt "#32379" +msgid "Genre" +msgstr "Género" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Classificação do Conteúdo" + +msgctxt "#32381" +msgid "Network" +msgstr "Rede" + +msgctxt "#32382" +msgid "Collection" +msgstr "Coleção" + +msgctxt "#32383" +msgid "Director" +msgstr "Diretor" + +msgctxt "#32384" +msgid "Actor" +msgstr "Ator" + +msgctxt "#32385" +msgid "Country" +msgstr "País" + +msgctxt "#32386" +msgid "Studio" +msgstr "Estúdio" + +msgctxt "#32387" +msgid "Labels" +msgstr "Rótulos" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Fabricante da câmara" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Modelo da câmara" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Abertura" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Velocidade do Obturador" + +msgctxt "#32392" +msgid "Lens" +msgstr "Lente" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Séries TV" + +msgctxt "#32394" +msgid "Music" +msgstr "Música" + +msgctxt "#32395" +msgid "Audio" +msgstr "Áudio" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Legendas" + +msgctxt "#32397" +msgid "Quality" +msgstr "Qualidade" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Configurações de Vídeo do Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Configurações de Áudio do Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Ir para Temporada" + +msgctxt "#32401" +msgid "Directors" +msgstr "Diretores" + +msgctxt "#32402" +msgid "Writer" +msgstr "Escritor" + +msgctxt "#32403" +msgid "Writers" +msgstr "Escritores" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Filmes relacionados" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Transferir Legendas" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Atraso da Legenda" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Próxima Legenda" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Desativar Legendas" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Ativar Legendas" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Versão da Plataforma" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Desconhecido" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Editar ou Limpar" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Editar endereço de IP ou limpar configurações atuais?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Limpar" + +msgctxt "#32415" +msgid "Edit" +msgstr "Editar" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Insira o endereço de IP" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Insira o número da Porta" + +msgctxt "#32418" +msgid "Creator" +msgstr "Criador" + +msgctxt "#32419" +msgid "Cast" +msgstr "Elenco" + +msgctxt "#32420" +msgid "Disc" +msgstr "Disco" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Terminar Sessão" + +msgctxt "#32422" +msgid "Exit" +msgstr "Sair" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Desligar" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Suspender" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Hibernar" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Reiniciar" + +msgctxt "#32427" +msgid "Failed" +msgstr "Falhou" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Falha ao iniciar sessão!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Resumir de {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Descobrir" + +msgctxt "#32431" +msgid "Search" +msgstr "Procurar" + +msgctxt "#32432" +msgid "Space" +msgstr "Espaço" + +msgctxt "#32433" +msgid "Clear" +msgstr "Limpar" + +msgctxt "#32434" +msgid "Searching..." +msgstr "A Procurar..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Sem Resultados" + +msgctxt "#32436" +msgid "Paused" +msgstr "Em Pausa" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Bem-Vindo" + +msgctxt "#32438" +msgid "Previous" +msgstr "Anterior" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Reproduzindo a seguir" + +msgctxt "#32440" +msgid "On Deck" +msgstr "Próximos a Ver" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Desconhecido" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Embutido" + +msgctxt "#32443" +msgid "Forced" +msgstr "Forçado" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Letra da música" + +msgctxt "#32445" +msgid "Mono" +msgstr "Mono" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Estéreo" + +msgctxt "#32447" +msgid "None" +msgstr "Nenhum" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Falha ao Reproduzir!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Não foi possivel ligar ao plex.tv[CR]Teste a ligação de internet e tente novamente." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Escolher Versão" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Reproduzir Versão..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "Nenhum conteúdo disponível nesta biblioteca" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Por favor adicione e/ou confirme que 'Incluir no Painel' está desativado." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Nenhum conteúdo disponível para este filtro" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Por favor, troque ou remova o filtro atual" + +msgctxt "#32456" +msgid "Show" +msgstr "Série" + +msgctxt "#32457" +msgid "By Show" +msgstr "Por Série" + +msgctxt "#32458" +msgid "Episodes" +msgstr "Episódios" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Modo Offline" + +msgctxt "#32460" +msgid "Sign In" +msgstr "Iniciar Sessão" + +msgctxt "#32461" +msgid "Albums" +msgstr "Álbuns" + +msgctxt "#32462" +msgid "Artist" +msgstr "Artista" + +msgctxt "#32463" +msgid "By Artist" +msgstr "Por Artista" + +msgctxt "#32464" +msgid "Player" +msgstr "Reprodutor" + +msgctxt "#32465" +msgid "Use skip step settings from Kodi" +msgstr "Use as definições de saltar etapas do Kodi" + +msgctxt "#32466" +msgid "Automatically seek selected position after a delay" +msgstr "Procurar automaticamente a posição selecionada após atrasar" + +msgctxt "#32467" +msgid "User Interface" +msgstr "Interface de Utilizador" + +msgctxt "#32471" +msgid "Use Plex/Kodi steps for timeline" +msgstr "Use as etapas do Plex/Kodi para a linha de tempo" + +msgctxt "#32485" +msgid "Go back instantly with the previous menu action in scrolled views" +msgstr "Recuar instantaneamente com a ação recuar do menu em visualizações deslocáveis" + +msgctxt "#32492" +msgid "Kodi Subtitle Settings" +msgstr "Definições de Legendas do Kodi" diff --git a/script.plexmod/resources/language/resource.language.ru_ru/strings.po b/script.plexmod/resources/language/resource.language.ru_ru/strings.po new file mode 100644 index 000000000..50b5ba08d --- /dev/null +++ b/script.plexmod/resources/language/resource.language.ru_ru/strings.po @@ -0,0 +1,951 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2013-12-12 22:56+0000\n" +"PO-Revision-Date: 2018-01-15 11:26+0300\n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Last-Translator: \n" +"X-Generator: Poedit 2.0.5\n" + +msgctxt "#32000" +msgid "Main" +msgstr "Главный" + +msgctxt "#32001" +msgid "Original" +msgstr "Оригинал" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "20 Мбит/с 1080p" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "12 Мбит/с 1080p" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "10 Мбит/с 1080p" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "8 Мбит/с 1080p" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "4 Мбит/с 720p" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "3 Мбит/с 720p" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "2 Мбит/с 720p" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "1.5 Мбит/с 480p" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "720 кбит/с" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "320 кбит/с" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "208 кбит/с" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "96 кбит/с" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "64 кбит/с" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "Локальное качество" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "Удаленное качество" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "Онлайн качество" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "Формат перекодирования" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "Отладка" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "Разрешить прямое воспроизведение" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "Разрешить прямую трансляцию" + +msgctxt "#32027" +msgid "Force" +msgstr "Принудительно" + +msgctxt "#32028" +msgid "Always" +msgstr "Всегда" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "Только изображения" + +msgctxt "#32030" +msgid "Auto" +msgstr "Авто" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "Наложение субтитров (только прямое воспроизведение)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "Разрешить небезопасные соединения" + +msgctxt "#32033" +msgid "Never" +msgstr "Никогда" + +msgctxt "#32034" +msgid "On Same network" +msgstr "В той же сети" + +msgctxt "#32035" +msgid "Always" +msgstr "Всегда" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "Разрешить 4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "Разрешить HEVC (h265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "Автоматический вход" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "Автоматическое воспроизведение" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "Включить скачивание субтитров" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "Включить скачивание субтитров" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "Обнаружение сервера (GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "Запускать Plex при старте Kodi" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "Соединение 1 IP" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "Соединение 1 порт" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "Соединение 2 IP" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "Соединение 2 порт" + +msgctxt "#32048" +msgid "Audio" +msgstr "Аудио" + +msgctxt "#32049" +msgid "Advanced" +msgstr "Продвинутый" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "Задать сервер вручную" + +msgctxt "#32051" +msgid "Privacy" +msgstr "Приватность" + +msgctxt "#32052" +msgid "About" +msgstr "Сведения" + +msgctxt "#32053" +msgid "Video" +msgstr "Видео" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "Версия аддона" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Версия Kodi" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "Разрешение экрана" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "Версия сервера" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "Пропустить выбор пользователя и ввод пин-кода при старте." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "Если включено, после окончании воспроизведения, если есть следующая запись, она будет автоматически запущена после 15 секундной задержки." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "Включите, если ваше оборудование поддерживает воспроизведение 4K. Отключите, чтобы использовать преобразование." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "Включите, если ваше оборудование поддерживает HEVC/h265. Отключите, чтобы использовать преобразование." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "Когда подключаться к серверам, не поддерживающим безопасные соединения.[CR][CR]* [B]Никогда[/B]: Никогда не подключаться к серверам с небезопасным соединением[CR]* [B]В той же сети[/B]: Разрешить, если сервер находится в той же сети[CR]* [B]Всегда[/B]: Разрешить в той же сети и удаленные соединения" + +msgctxt "#32201" +msgid "Trailer" +msgstr "Трейлер" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "Удаленные эпизоды" + +msgctxt "#32203" +msgid "Interview" +msgstr "Интервью" + +msgctxt "#32204" +msgid "Music Video" +msgstr "Клип" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "За кулисами" + +msgctxt "#32206" +msgid "Scene" +msgstr "Эпизод" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "Живая музыка" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "Лирический клип" + +msgctxt "#32209" +msgid "Concert" +msgstr "Концерт" + +msgctxt "#32210" +msgid "Featurette" +msgstr "Короткометражный фильм" + +msgctxt "#32211" +msgid "Short" +msgstr "Короткий" + +msgctxt "#32212" +msgid "Other" +msgstr "Прочее" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "Перейти к альбому" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "Перейти к артисту" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "Перейти к {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "Сезон" + +msgctxt "#32304" +msgid "Episode" +msgstr "Эпизод" + +msgctxt "#32305" +msgid "Extras" +msgstr "Дополнения" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "Связанные вупыски" + +msgctxt "#32307" +msgid "More" +msgstr "Еще" + +msgctxt "#32308" +msgid "Available" +msgstr "Доступно" + +msgctxt "#32309" +msgid "None" +msgstr "Нет" + +msgctxt "#32310" +msgid "S" +msgstr "S" + +msgctxt "#32311" +msgid "E" +msgstr "E" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "Недосутпно" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "Эта запись сейчас недоступна." + +msgctxt "#32314" +msgid "In Progress" +msgstr "В работе" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "Возобновить воспроизведение?" + +msgctxt "#32316" +msgid "Resume" +msgstr "Возобновить" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "Начать сначала" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "Пометить как не просмотренное" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "Пометить как просмотренное" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "Пометить сезон как не просмотренный" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "Пометить сезон как просмотренный" + +msgctxt "#32322" +msgid "Delete" +msgstr "Удалить" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "Перейти к просмотру" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "Перейти к {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "Воспроизвести следующий" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "Действительно удалить?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "Вы действительно хотите удалить?" + +msgctxt "#32328" +msgid "Yes" +msgstr "Да" + +msgctxt "#32329" +msgid "No" +msgstr "Нет" + +msgctxt "#32330" +msgid "Message" +msgstr "Сообщение" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "Возникла проблема при удалении." + +msgctxt "#32332" +msgid "Home" +msgstr "Домой" + +msgctxt "#32333" +msgid "Playlists" +msgstr "Плейлисты" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "Подтверждение выхода" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "Вы действительно хотите выйти из Plex?" + +msgctxt "#32336" +msgid "Exit" +msgstr "Выход" + +msgctxt "#32337" +msgid "Cancel" +msgstr "Отмена" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "Сервера не найдены" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "Сервер недоступен" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "Пожалуйста, подождите, идет проверка соединения." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "Сервер недоступен. Пожалуйста, войдите на ваш сервер и проверьте соединение." + +msgctxt "#32342" +msgid "Switch User" +msgstr "Сменить пользователя" + +msgctxt "#32343" +msgid "Settings" +msgstr "Настройки" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "Выйти" + +msgctxt "#32345" +msgid "All" +msgstr "Все" + +msgctxt "#32346" +msgid "By Name" +msgstr "По имени" + +msgctxt "#32347" +msgid "Artists" +msgstr "Артисты" + +msgctxt "#32348" +msgid "movies" +msgstr "фильмы" + +msgctxt "#32349" +msgid "photos" +msgstr "фото" + +msgctxt "#32350" +msgid "Shows" +msgstr "Сериалы" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "По дате добавления" + +msgctxt "#32352" +msgid "Date Added" +msgstr "Дата добавления" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "По дате выхода" + +msgctxt "#32354" +msgid "Release Date" +msgstr "Дата выхода" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "По дате просмотра" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "Дата просмотра" + +msgctxt "#32357" +msgid "By Name" +msgstr "По имени" + +msgctxt "#32358" +msgid "Name" +msgstr "Имя" + +msgctxt "#32359" +msgid "By Rating" +msgstr "По рейтингу" + +msgctxt "#32360" +msgid "Rating" +msgstr "Рейтинг" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "По разрешению" + +msgctxt "#32362" +msgid "Resolution" +msgstr "Разрешение" + +msgctxt "#32363" +msgid "By Duration" +msgstr "По длительности" + +msgctxt "#32364" +msgid "Duration" +msgstr "Длительность" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "По дате показа" + +msgctxt "#32366" +msgid "First Aired" +msgstr "Дата показа" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "По не просмотренным" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "Не просмотренные" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "По дате воспроизведения" + +msgctxt "#32370" +msgid "Date Played" +msgstr "Дата воспроизведения" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "По количеству просмотров" + +msgctxt "#32372" +msgid "Play Count" +msgstr "Количество просмотров" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "По выбранной дате" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "Выбранная дата" + +msgctxt "#32375" +msgid "No filters available" +msgstr "Нет фильтров" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "Очистить фильтр" + +msgctxt "#32377" +msgid "Year" +msgstr "Год" + +msgctxt "#32378" +msgid "Decade" +msgstr "Десятилетие" + +msgctxt "#32379" +msgid "Genre" +msgstr "Жанр" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "Рейтинг контента" + +msgctxt "#32381" +msgid "Network" +msgstr "Сеть" + +msgctxt "#32382" +msgid "Collection" +msgstr "Коллекция" + +msgctxt "#32383" +msgid "Director" +msgstr "Режиссер" + +msgctxt "#32384" +msgid "Actor" +msgstr "Актер" + +msgctxt "#32385" +msgid "Country" +msgstr "Страна" + +msgctxt "#32386" +msgid "Studio" +msgstr "Студия" + +msgctxt "#32387" +msgid "Labels" +msgstr "Лейбл" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "Камера" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "Модель камеры" + +msgctxt "#32390" +msgid "Aperture" +msgstr "Диафрагма" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "Скорость затвора" + +msgctxt "#32392" +msgid "Lens" +msgstr "Объектив" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "Сериалы" + +msgctxt "#32394" +msgid "Music" +msgstr "Музыка" + +msgctxt "#32395" +msgid "Audio" +msgstr "Аудио" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "Субтитры" + +msgctxt "#32397" +msgid "Quality" +msgstr "Качетсво" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "Настройки видео Kodi" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "Настройки аудио Kodi" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "Перейти к сезону" + +msgctxt "#32401" +msgid "Directors" +msgstr "Режиссеры" + +msgctxt "#32402" +msgid "Writer" +msgstr "Сценарист" + +msgctxt "#32403" +msgid "Writers" +msgstr "Сценаристы" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "Похожие фильмы" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "Скачать субтитры" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "Задержка субтитров" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "Следующие субтитры" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "Отключить субтитры" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "Включить субтитры" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "Версия платформы" + +msgctxt "#32411" +msgid "Unknown" +msgstr "Неизсвестно" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "Изменить или удалить" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "Изменить IP адрес или удалить настройки?" + +msgctxt "#32414" +msgid "Clear" +msgstr "Удалить" + +msgctxt "#32415" +msgid "Edit" +msgstr "Изменить" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "Введите IP адрес" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "Введите номер порта" + +msgctxt "#32418" +msgid "Creator" +msgstr "Автор" + +msgctxt "#32419" +msgid "Cast" +msgstr "В ролях" + +msgctxt "#32420" +msgid "Disc" +msgstr "Диск" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "Выйти из учетной записи" + +msgctxt "#32422" +msgid "Exit" +msgstr "Выход" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "Завершить работу" + +msgctxt "#32424" +msgid "Suspend" +msgstr "Перейти в режим сна" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "Гибернация" + +msgctxt "#32426" +msgid "Reboot" +msgstr "Перезагрузка" + +msgctxt "#32427" +msgid "Failed" +msgstr "Ошибка" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "Ошибка входа!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "Начать с {0}" + +msgctxt "#32430" +msgid "Discovery" +msgstr "Обнаружение" + +msgctxt "#32431" +msgid "Search" +msgstr "Поиск" + +msgctxt "#32432" +msgid "Space" +msgstr "Пробел" + +msgctxt "#32433" +msgid "Clear" +msgstr "Удалить" + +msgctxt "#32434" +msgid "Searching..." +msgstr "Поиск..." + +msgctxt "#32435" +msgid "No Results" +msgstr "Нет результатов" + +msgctxt "#32436" +msgid "Paused" +msgstr "Пауза" + +msgctxt "#32437" +msgid "Welcome" +msgstr "Добро пожаловать" + +msgctxt "#32438" +msgid "Previous" +msgstr "Предыдущий" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "Следующий" + +msgctxt "#32440" +msgid "On Deck" +msgstr "Сейчас просматривается" + +msgctxt "#32441" +msgid "Unknown" +msgstr "Неизвестно" + +msgctxt "#32442" +msgid "Embedded" +msgstr "Встроенный" + +msgctxt "#32443" +msgid "Forced" +msgstr "Принудительно" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "Текст песни" + +msgctxt "#32445" +msgid "Mono" +msgstr "Моно" + +msgctxt "#32446" +msgid "Stereo" +msgstr "Стерео" + +msgctxt "#32447" +msgid "None" +msgstr "Нет" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "Ошибка воспроизведения!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "Нет соединения с plex.tv[CR]Проверьте ваше интернет-соединение и попробуйте еще раз." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "Выберите версию" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "Версия воспроизведения..." + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "В этой библиотеке ничего нет" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "Пожалуйста, добавьте контент и/или проверьте включено ли \"Добавить на панель\"." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "Нет результатов с выбранными условиями" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "Пожалуйста, измените или удалите текущий фильтр" + +msgctxt "#32456" +msgid "Show" +msgstr "Показ" + +msgctxt "#32457" +msgid "By Show" +msgstr "По показу" + +msgctxt "#32458" +msgid "Episodes" +msgstr "Эпизоды" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "Автономный режим" + +msgctxt "#32460" +msgid "Sign In" +msgstr "Войти" + +msgctxt "#32461" +msgid "Albums" +msgstr "Альбомы" + +msgctxt "#32462" +msgid "Artist" +msgstr "Артист" + +msgctxt "#32463" +msgid "By Artist" +msgstr "По артисту" diff --git a/script.plexmod/resources/language/resource.language.zh_cn/strings.po b/script.plexmod/resources/language/resource.language.zh_cn/strings.po new file mode 100644 index 000000000..46b8c93da --- /dev/null +++ b/script.plexmod/resources/language/resource.language.zh_cn/strings.po @@ -0,0 +1,1075 @@ +# XBMC Media Center language file +msgid "" +msgstr "" +"Project-Id-Version: XBMC-Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: 2020-3-4 22:56+0000\n" +"PO-Revision-Date: 2022-02-08 19:00+0800\n" +"Last-Translator: Qianyu Li \n" +"Language-Team: LANGUAGE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgctxt "#32000" +msgid "Main" +msgstr "一般" + +msgctxt "#32001" +msgid "Original" +msgstr "原始" + +msgctxt "#32002" +msgid "20 Mbps 1080p" +msgstr "" + +msgctxt "#32003" +msgid "12 Mbps 1080p" +msgstr "" + +msgctxt "#32004" +msgid "10 Mbps 1080p" +msgstr "" + +msgctxt "#32005" +msgid "8 Mbps 1080p" +msgstr "" + +msgctxt "#32006" +msgid "4 Mbps 720p" +msgstr "" + +msgctxt "#32007" +msgid "3 Mbps 720p" +msgstr "" + +msgctxt "#32008" +msgid "2 Mbps 720p" +msgstr "" + +msgctxt "#32009" +msgid "1.5 Mbps 480p" +msgstr "" + +msgctxt "#32010" +msgid "720 kbps" +msgstr "" + +msgctxt "#32011" +msgid "320 kbps" +msgstr "" + +msgctxt "#32012" +msgid "208 kbps" +msgstr "" + +msgctxt "#32013" +msgid "96 kbps" +msgstr "" + +msgctxt "#32014" +msgid "64 kbps" +msgstr "" + +msgctxt "#32020" +msgid "Local Quality" +msgstr "本地播放质量" + +msgctxt "#32021" +msgid "Remote Quality" +msgstr "远程播放质量" + +msgctxt "#32022" +msgid "Online Quality" +msgstr "在线播放质量" + +msgctxt "#32023" +msgid "Transcode Format" +msgstr "转码格式" + +msgctxt "#32024" +msgid "Debug Logging" +msgstr "调试日志" + +msgctxt "#32025" +msgid "Allow Direct Play" +msgstr "允许直接播放" + +msgctxt "#32026" +msgid "Allow Direct Stream" +msgstr "允许直连串流" + +msgctxt "#32027" +msgid "Force" +msgstr "强制直接播放" + +msgctxt "#32028" +msgid "Always" +msgstr "总是" + +msgctxt "#32029" +msgid "Only Image Formats" +msgstr "仅图像格式" + +msgctxt "#32030" +msgid "Auto" +msgstr "自动" + +msgctxt "#32031" +msgid "Burn Subtitles (Direct Play Only)" +msgstr "固化字幕(仅直接播放)" + +msgctxt "#32032" +msgid "Allow Insecure Connections" +msgstr "允许非加密连接" + +msgctxt "#32033" +msgid "Never" +msgstr "从不" + +msgctxt "#32034" +msgid "On Same network" +msgstr "在同一网络" + +msgctxt "#32035" +msgid "Always" +msgstr "总是" + +msgctxt "#32036" +msgid "Allow 4K" +msgstr "允许4K" + +msgctxt "#32037" +msgid "Allow HEVC (h265)" +msgstr "允许HEVC (h265)" + +msgctxt "#32038" +msgid "Automatically Sign In" +msgstr "自动登录" + +msgctxt "#32039" +msgid "Post Play Auto Play" +msgstr "点击海报自动播放" + +msgctxt "#32040" +msgid "Enable Subtitle Downloading" +msgstr "允许下载字幕" + +msgctxt "#32041" +msgid "Enable Subtitle Downloading" +msgstr "允许下载字幕" + +msgctxt "#32042" +msgid "Server Discovery (GDM)" +msgstr "发现服务器(GDM)" + +msgctxt "#32043" +msgid "Start Plex On Kodi Startup" +msgstr "打开Kodi时自动启动PLEX" + +msgctxt "#32044" +msgid "Connection 1 IP" +msgstr "服务器1的IP" + +msgctxt "#32045" +msgid "Connection 1 Port" +msgstr "服务器1的端口" + +msgctxt "#32046" +msgid "Connection 2 IP" +msgstr "服务器2的IP" + +msgctxt "#32047" +msgid "Connection 2 Port" +msgstr "服务器2的端口" + +msgctxt "#32048" +msgid "Audio" +msgstr "音频" + +msgctxt "#32049" +msgid "Advanced" +msgstr "高级" + +msgctxt "#32050" +msgid "Manual Servers" +msgstr "手动输入服务器地址" + +msgctxt "#32051" +msgid "Privacy" +msgstr "隐私" + +msgctxt "#32052" +msgid "About" +msgstr "关于" + +msgctxt "#32053" +msgid "Video" +msgstr "视频" + +msgctxt "#32054" +msgid "Addon Version" +msgstr "插件版本" + +msgctxt "#32055" +msgid "Kodi Version" +msgstr "Kodi版本" + +msgctxt "#32056" +msgid "Screen Resolution" +msgstr "屏幕分辨率" + +msgctxt "#32057" +msgid "Current Server Version" +msgstr "PLEX服务器版本" + +msgctxt "#32100" +msgid "Skip user selection and pin entry on startup." +msgstr "启动时跳过选择用户和图钉." + +msgctxt "#32101" +msgid "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatically be played after a 15 second delay." +msgstr "如果启用,当播放结束并且有一个“Next Up”项目可用时,它将在15秒的延迟后自动播放." + +msgctxt "#32102" +msgid "Enable this if your hardware can handle 4K playback. Disable it to force transcoding." +msgstr "如果您的硬件可以处理4K播放,请启用此选项.禁用它以强制转码." + +msgctxt "#32103" +msgid "Enable this if your hardware can handle HEVC/h265. Disable it to force transcoding." +msgstr "如果您的硬件可以处理HEVC/h265,请启用此选项.禁用它以强制转码." + +msgctxt "#32104" +msgid "When to connect to servers with no secure connections.[CR][CR]* [B]Never[/B]: Never connect to a server insecurely[CR]* [B]On Same Network[/B]: Allow if on the same network[CR]* [B]Always[/B]: Allow same network and remote connections" +msgstr "何时连接到没有安全连接的服务器。[CR][CR]* [B]从不[/B]: 从不以不安全方式连接服务器[CR]* [B]在同一网络[/B]: 允许同一网络时[CR]* [B]总是[/B]: 允许同一网络和远程连接时" + +msgctxt "#32201" +msgid "Trailer" +msgstr "预告片" + +msgctxt "#32202" +msgid "Deleted Scene" +msgstr "删除的场景" + +msgctxt "#32203" +msgid "Interview" +msgstr "访问" + +msgctxt "#32204" +msgid "Music Video" +msgstr "音乐视频" + +msgctxt "#32205" +msgid "Behind the Scenes" +msgstr "幕后花絮" + +msgctxt "#32206" +msgid "Scene" +msgstr "现场" + +msgctxt "#32207" +msgid "Live Music Video" +msgstr "现场音乐视频" + +msgctxt "#32208" +msgid "Lyric Music Video" +msgstr "歌词音乐视频" + +msgctxt "#32209" +msgid "Concert" +msgstr "演奏会" + +msgctxt "#32210" +msgid "Featurette" +msgstr "电影短片" + +msgctxt "#32211" +msgid "Short" +msgstr "短篇" + +msgctxt "#32212" +msgid "Other" +msgstr "其他" + +msgctxt "#32300" +msgid "Go to Album" +msgstr "跳转到相册" + +msgctxt "#32301" +msgid "Go to Artist" +msgstr "跳转到演员" + +msgctxt "#32302" +msgid "Go to {0}" +msgstr "跳转到 {0}" + +msgctxt "#32303" +msgid "Season" +msgstr "季数" + +msgctxt "#32304" +msgid "Episode" +msgstr "集数" + +msgctxt "#32305" +msgid "Extras" +msgstr "其他" + +msgctxt "#32306" +msgid "Related Shows" +msgstr "相关节目" + +msgctxt "#32307" +msgid "More" +msgstr "更多" + +msgctxt "#32308" +msgid "Available" +msgstr "可用的" + +msgctxt "#32309" +msgid "None" +msgstr "无" + +msgctxt "#32310" +msgid "S" +msgstr "" + +msgctxt "#32311" +msgid "E" +msgstr "" + +msgctxt "#32312" +msgid "Unavailable" +msgstr "不可用" + +msgctxt "#32313" +msgid "This item is currently unavailable." +msgstr "这个项目现在不可用." + +msgctxt "#32314" +msgid "In Progress" +msgstr "进行中" + +msgctxt "#32315" +msgid "Resume playback?" +msgstr "恢复播放?" + +msgctxt "#32316" +msgid "Resume" +msgstr "恢复" + +msgctxt "#32317" +msgid "Play from beginning" +msgstr "从头播放" + +msgctxt "#32318" +msgid "Mark Unplayed" +msgstr "标记为未播放" + +msgctxt "#32319" +msgid "Mark Played" +msgstr "标记为已播放" + +msgctxt "#32320" +msgid "Mark Season Unplayed" +msgstr "标记整季未播放" + +msgctxt "#32321" +msgid "Mark Season Played" +msgstr "标记整季已播放" + +msgctxt "#32322" +msgid "Delete" +msgstr "删除" + +msgctxt "#32323" +msgid "Go To Show" +msgstr "转到剧集" + +msgctxt "#32324" +msgid "Go To {0}" +msgstr "跳转到 {0}" + +msgctxt "#32325" +msgid "Play Next" +msgstr "播放下一个" + +msgctxt "#32326" +msgid "Really Delete?" +msgstr "确定删除?" + +msgctxt "#32327" +msgid "Are you sure you really want to delete this media?" +msgstr "你确定你要删除这个媒体么?" + +msgctxt "#32328" +msgid "Yes" +msgstr "是" + +msgctxt "#32329" +msgid "No" +msgstr "否" + +msgctxt "#32330" +msgid "Message" +msgstr "消息" + +msgctxt "#32331" +msgid "There was a problem while attempting to delete the media." +msgstr "尝试删除媒体时出现问题." + +msgctxt "#32332" +msgid "Home" +msgstr "主页" + +msgctxt "#32333" +msgid "Playlists" +msgstr "播放列表" + +msgctxt "#32334" +msgid "Confirm Exit" +msgstr "确定退出" + +msgctxt "#32335" +msgid "Are you ready to exit Plex?" +msgstr "你确定要退出PLEX么?" + +msgctxt "#32336" +msgid "Exit" +msgstr "退出" + +msgctxt "#32337" +msgid "Cancel" +msgstr "返回" + +msgctxt "#32338" +msgid "No Servers Found" +msgstr "没有找到服务器" + +msgctxt "#32339" +msgid "Server is not accessible" +msgstr "服务器不能访问" + +msgctxt "#32340" +msgid "Connection tests are in progress. Please wait." +msgstr "连接测试正在进行中.请耐心等待." + +msgctxt "#32341" +msgid "Server is not accessible. Please sign into your server and check your connection." +msgstr "服务器不可访问.请登录您的服务器并检查您的连接." + +msgctxt "#32342" +msgid "Switch User" +msgstr "切换用户" + +msgctxt "#32343" +msgid "Settings" +msgstr "设置" + +msgctxt "#32344" +msgid "Sign Out" +msgstr "登出" + +msgctxt "#32345" +msgid "All" +msgstr "所有" + +msgctxt "#32346" +msgid "By Name" +msgstr "按名称排序" + +msgctxt "#32347" +msgid "Artists" +msgstr "艺人" + +msgctxt "#32348" +msgid "Movies" +msgstr "电影" + +msgctxt "#32349" +msgid "photos" +msgstr "相片" + +msgctxt "#32350" +msgid "Shows" +msgstr "演出" + +msgctxt "#32351" +msgid "By Date Added" +msgstr "按添加日期排序" + +msgctxt "#32352" +msgid "Date Added" +msgstr "添加日期" + +msgctxt "#32353" +msgid "By Release Date" +msgstr "按发布日期排序" + +msgctxt "#32354" +msgid "Release Date" +msgstr "发布日期" + +msgctxt "#32355" +msgid "By Date Viewed" +msgstr "按观看日期排序" + +msgctxt "#32356" +msgid "Date Viewed" +msgstr "观看日期" + +msgctxt "#32357" +msgid "By Name" +msgstr "按名称排序" + +msgctxt "#32358" +msgid "Name" +msgstr "名称" + +msgctxt "#32359" +msgid "By Rating" +msgstr "按热度排序" + +msgctxt "#32360" +msgid "Rating" +msgstr "热度" + +msgctxt "#32361" +msgid "By Resolution" +msgstr "按分辨率排序" + +msgctxt "#32362" +msgid "Resolution" +msgstr "分辨率" + +msgctxt "#32363" +msgid "By Duration" +msgstr "按时长排序" + +msgctxt "#32364" +msgid "Duration" +msgstr "时长" + +msgctxt "#32365" +msgid "By First Aired" +msgstr "按首次播出排序" + +msgctxt "#32366" +msgid "First Aired" +msgstr "首次播出" + +msgctxt "#32367" +msgid "By Unplayed" +msgstr "按未播放排序" + +msgctxt "#32368" +msgid "Unplayed" +msgstr "未播放" + +msgctxt "#32369" +msgid "By Date Played" +msgstr "按播放日期排序" + +msgctxt "#32370" +msgid "Date Played" +msgstr "播放日期" + +msgctxt "#32371" +msgid "By Play Count" +msgstr "按播放次数排序" + +msgctxt "#32372" +msgid "Play Count" +msgstr "播放次数" + +msgctxt "#32373" +msgid "By Date Taken" +msgstr "按拍摄日期排序" + +msgctxt "#32374" +msgid "Date Taken" +msgstr "拍摄日期" + +msgctxt "#32375" +msgid "No filters available" +msgstr "没找到相关内容" + +msgctxt "#32376" +msgid "Clear Filter" +msgstr "清空内容" + +msgctxt "#32377" +msgid "Year" +msgstr "年" + +msgctxt "#32378" +msgid "Decade" +msgstr "年代" + +msgctxt "#32379" +msgid "Genre" +msgstr "类型" + +msgctxt "#32380" +msgid "Content Rating" +msgstr "内容分级" + +msgctxt "#32381" +msgid "Network" +msgstr "网络" + +msgctxt "#32382" +msgid "Collection" +msgstr "合集" + +msgctxt "#32383" +msgid "Director" +msgstr "导演" + +msgctxt "#32384" +msgid "Actor" +msgstr "演员" + +msgctxt "#32385" +msgid "Country" +msgstr "国家" + +msgctxt "#32386" +msgid "Studio" +msgstr "工作室" + +msgctxt "#32387" +msgid "Labels" +msgstr "标签" + +msgctxt "#32388" +msgid "Camera Make" +msgstr "相机品牌" + +msgctxt "#32389" +msgid "Camera Model" +msgstr "相机型号" + +msgctxt "#32390" +msgid "Aperture" +msgstr "光圈" + +msgctxt "#32391" +msgid "Shutter Speed" +msgstr "快门速度" + +msgctxt "#32392" +msgid "Lens" +msgstr "镜头" + +msgctxt "#32393" +msgid "TV Shows" +msgstr "剧集" + +msgctxt "#32394" +msgid "Music" +msgstr "音乐" + +msgctxt "#32395" +msgid "Audio" +msgstr "音频" + +msgctxt "#32396" +msgid "Subtitles" +msgstr "字幕" + +msgctxt "#32397" +msgid "Quality" +msgstr "质量" + +msgctxt "#32398" +msgid "Kodi Video Settings" +msgstr "KODI视频设置" + +msgctxt "#32399" +msgid "Kodi Audio Settings" +msgstr "KODI音频设置" + +msgctxt "#32400" +msgid "Go To Season" +msgstr "跳转到季" + +msgctxt "#32401" +msgid "Directors" +msgstr "导演" + +msgctxt "#32402" +msgid "Writer" +msgstr "编剧" + +msgctxt "#32403" +msgid "Writers" +msgstr "编剧" + +msgctxt "#32404" +msgid "Related Movies" +msgstr "相关电影" + +msgctxt "#32405" +msgid "Download Subtitles" +msgstr "下载字幕" + +msgctxt "#32406" +msgid "Subtitle Delay" +msgstr "字幕延迟" + +msgctxt "#32407" +msgid "Next Subtitle" +msgstr "下一个字幕" + +msgctxt "#32408" +msgid "Disable Subtitles" +msgstr "禁用字幕" + +msgctxt "#32409" +msgid "Enable Subtitles" +msgstr "允许字幕" + +msgctxt "#32410" +msgid "Platform Version" +msgstr "平台版本" + +msgctxt "#32411" +msgid "Unknown" +msgstr "未知" + +msgctxt "#32412" +msgid "Edit Or Clear" +msgstr "编辑或清除" + +msgctxt "#32413" +msgid "Edit IP address or clear the current setting?" +msgstr "编辑IP地址或清除当前设置?" + +msgctxt "#32414" +msgid "Clear" +msgstr "清除" + +msgctxt "#32415" +msgid "Edit" +msgstr "编辑" + +msgctxt "#32416" +msgid "Enter IP Address" +msgstr "填写IP地址" + +msgctxt "#32417" +msgid "Enter Port Number" +msgstr "填写端口" + +msgctxt "#32418" +msgid "Creator" +msgstr "创作者" + +msgctxt "#32419" +msgid "Cast" +msgstr "阵容" + +msgctxt "#32420" +msgid "Disc" +msgstr "碟片" + +msgctxt "#32421" +msgid "Sign Out" +msgstr "登出" + +msgctxt "#32422" +msgid "Exit" +msgstr "离开" + +msgctxt "#32423" +msgid "Shutdown" +msgstr "关机" + +msgctxt "#32424" +msgid "Suspend" +msgstr "暂停" + +msgctxt "#32425" +msgid "Hibernate" +msgstr "休眠" + +msgctxt "#32426" +msgid "Reboot" +msgstr "重启" + +msgctxt "#32427" +msgid "Failed" +msgstr "失败" + +msgctxt "#32428" +msgid "Login failed!" +msgstr "登陆失败!" + +msgctxt "#32429" +msgid "Resume from {0}" +msgstr "从{0}恢复" + +msgctxt "#32430" +msgid "Discovery" +msgstr "发现" + +msgctxt "#32431" +msgid "Search" +msgstr "搜索" + +msgctxt "#32432" +msgid "Space" +msgstr "空格" + +msgctxt "#32433" +msgid "Clear" +msgstr "清除" + +msgctxt "#32434" +msgid "Searching..." +msgstr "搜索中..." + +msgctxt "#32435" +msgid "No Results" +msgstr "无结果" + +msgctxt "#32436" +msgid "Paused" +msgstr "暂停" + +msgctxt "#32437" +msgid "Welcome" +msgstr "欢迎" + +msgctxt "#32438" +msgid "Previous" +msgstr "重播" + +msgctxt "#32439" +msgid "Playing Next" +msgstr "播放下一集" + +msgctxt "#32440" +msgid "On Deck" +msgstr "已播项目" + +msgctxt "#32441" +msgid "Unknown" +msgstr "未知" + +msgctxt "#32442" +msgid "Embedded" +msgstr "内嵌" + +msgctxt "#32443" +msgid "Forced" +msgstr "强制" + +msgctxt "#32444" +msgid "Lyrics" +msgstr "歌词" + +msgctxt "#32445" +msgid "Mono" +msgstr "单声道" + +msgctxt "#32446" +msgid "Stereo" +msgstr "立体声" + +msgctxt "#32447" +msgid "None" +msgstr "无" + +msgctxt "#32448" +msgid "Playback Failed!" +msgstr "播放失败!" + +msgctxt "#32449" +msgid "Can't connect to plex.tv[CR]Check your internet connection and try again." +msgstr "无法连接至plex.tv[CR]检查您的互联网连接,然后重试." + +msgctxt "#32450" +msgid "Choose Version" +msgstr "选择版本" + +msgctxt "#32451" +msgid "Play Version..." +msgstr "播放版本" + +msgctxt "#32452" +msgid "No Content available in this library" +msgstr "库里没有可播放的内容" + +msgctxt "#32453" +msgid "Please add content and/or check that 'Include in dashboard' is enabled." +msgstr "请添加内容和/或检查 '包括在仪表板' 是否已启用." + +msgctxt "#32454" +msgid "No Content available for this filter" +msgstr "没有可用于此过滤器的内容" + +msgctxt "#32455" +msgid "Please change change or remove the current filter" +msgstr "请更改或删除当前过滤器" + +msgctxt "#32456" +msgid "Show" +msgstr "节目" + +msgctxt "#32457" +msgid "By Show" +msgstr "按节目排序" + +msgctxt "#32458" +msgid "Episodes" +msgstr "集数" + +msgctxt "#32459" +msgid "Offline Mode" +msgstr "离线模式" + +msgctxt "#32460" +msgid "Sign In" +msgstr "登录" + +msgctxt "#32461" +msgid "Albums" +msgstr "专辑" + +msgctxt "#32462" +msgid "Artist" +msgstr "艺人" + +msgctxt "#32463" +msgid "By Artist" +msgstr "按艺人排序" + +msgctxt "#32464" +msgid "Player" +msgstr "播放" + +msgctxt "#32465" +msgid "Use skip step settings from Kodi" +msgstr "使用Kodi设置的跳过步骤" + +msgctxt "#32466" +msgid "Automatically seek selected position after a delay" +msgstr "延迟后自动跳到所选位置" + +msgctxt "#32467" +msgid "User Interface" +msgstr "用户界面" + +msgctxt "#32468" +msgid "Show dynamic background art" +msgstr "显示动态背景艺术" + +msgctxt "#32469" +msgid "Background art blur amount" +msgstr "背景艺术模糊量" + +msgctxt "#32470" +msgid "Background art opacity" +msgstr "背景艺术不透明度" + +msgctxt "#32471" +msgid "Use Plex/Kodi steps for timeline" +msgstr "使用Plex/Kodi的时间轴步骤" + +msgctxt "#32480" +msgid "Theme music" +msgstr "主题音乐" + +msgctxt "#32481" +msgid "Off" +msgstr "关闭" + +msgctxt "#32482" +msgid "%(percentage)s %%" +msgstr "" + +msgctxt "#32483" +msgid "Hide Stream Info" +msgstr "隐藏流信息" + +msgctxt "#32484" +msgid "Show Stream Info" +msgstr "显示流信息" + +msgctxt "#32485" +msgid "Go back instantly with the previous menu action in scrolled views" +msgstr "滚动视图中的上一个菜单操作后立即返回" + +msgctxt "#32487" +msgid "Seek Delay" +msgstr "跳过延迟" + +msgctxt "#32488" +msgid "Screensaver" +msgstr "屏幕保护" + +msgctxt "#32489" +msgid "Quiz Mode" +msgstr "测验模式" + +msgctxt "#32490" +msgid "Collections" +msgstr "合集" + +msgctxt "#32491" +msgid "Folders" +msgstr "文件夹" + +msgctxt "#32492" +msgid "Kodi Subtitle Settings" +msgstr "KODI字幕设置" + +msgctxt "#32493" +msgid "Prefer normal over forced subtitles if available (PMS selects forced by default)" +msgstr "如果有的话,优先选择正常字幕而不是强制字幕(PMS默认选择强制字幕)" + +msgctxt "#32495" +msgid "Skip intro" +msgstr "跳过片头" + +msgctxt "#32500" +msgid "Always show post-play screen (even for short videos)" +msgstr "始终显示播放后的画面(即使是短视频)" + +msgctxt "#32501" +msgid "Time-to-wait between videos on post-play" +msgstr "播放后的视频之间的等待时间" + +msgctxt "#32505" +msgid "Visit media in video playlist instead of playing it" +msgstr "访问视频播放列表中的媒体而不是播放它" + +msgctxt "#32601" +msgid "Allow AV1" +msgstr "允许AV1" + +msgctxt "#32602" +msgid "Enable this if your hardware can handle AV1. Disable it to force transcoding." +msgstr "如果你的硬件可以处理AV1,请启用此功能.禁用它可以强制进行转码." + +msgctxt "#32521" +msgid "Skip Intro Button Timeout" +msgstr "跳过片头按钮超时" + +msgctxt "#32522" +msgid "Automatically Skip Intro" +msgstr "自动跳过片头" + +msgctxt "#32523" +msgid "Automatically skip intros if available." +msgstr "如果可用既自动跳过片头." + +msgctxt "#32524" +msgid "Set how long the skip intro button shows for." +msgstr "设置跳过片头按钮的显示时间." + diff --git a/script.plexmod/resources/settings.xml b/script.plexmod/resources/settings.xml new file mode 100644 index 000000000..973bb4a12 --- /dev/null +++ b/script.plexmod/resources/settings.xml @@ -0,0 +1,335 @@ + + +
+ + + + + + + + + 0 + false + + + + 0 + false + + + + 0 + true + + + + 0 + 100 + + 25 + 25 + 750 + + + false + + + + + + + + + 0 + true + + + + 0 + 1 + + 0 + 0.1 + 5 + + + false + + + true + + + + 0 + false + + + + 0 + false + + + + 0 + true + + + + 0 + true + + + true + + + + 0 + true + + + + 0 + 120 + + 0 + 1 + 600 + + + false + + + + 0 + 10 + + 0 + 1 + 600 + + + false + + + + + + + + + 0 + 2.5 + + -10 + 0.5 + 10 + + + 33630 + false + + + + 0 + 10 + + 1 + 1 + 40 + + + 32524 + false + + + + 0 + 10 + + 1 + 1 + 40 + + + 32528 + false + + + + 0 + 60 + + 10 + 10 + 1800 + + + 33510 + false + + + + 0 + true + + + + 0 + false + + + + + + + + + 0 + false + + + + 0 + false + + + + 0 + true + + + + 0 + true + + + true + + + + 0 + - + + + false + + + + 0 + 0 + + 0 + 1 + 256 + + + false + + + true + + + + 0 + 20 + + 0 + 1 + 100 + + + false + + + true + + + + 0 + false + + + true + + + + 0 + true + + + + 0 + true + + + + 0 + 16 + + 32501 + + + + 0 + true + + + + 0 + true + + + + + + + + 0 + false + + + + + + + + 0 + 5 + + 0.1 + 0.1 + 20 + + + false + + + + 0 + 2.5 + + 0.1 + 0.1 + 20 + + + false + + + + 0 + 10 + + 1 + 1 + 1000 + + + false + + + + +
+
diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-album.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-album.xml new file mode 100644 index 000000000..e9c0d979a --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-album.xml @@ -0,0 +1,628 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + 0 + 135 + 101 + + + 60 + 0 + + 0 + 5 + 420 + 40 + font13 + left + center + FFFFFFFF + + + + 0 + 60 + 420 + 40 + font13 + left + center + FFFFFFFF + + + + 0 + 142 + 630 + 630 + $INFO[Window.Property(album.thumb)] + scale + + + + + VisibleChange + 301 + 50 + 784 + 650 + 145 + 200 + 101 + -50 + horizontal + center + 200 + true + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 750 + 0 + 1170 + 945 + + 0 + 0 + 1380 + 945 + script.plex/white-square.png + 20000000 + + + + 0 + 0 + 1170 + 945 + 200 + 152 + 300 + 200 + vertical + 4 + 152 + + + + String.IsEmpty(ListItem.Property(is.header)) + 120 + 24 + + !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + -10 + 0 + 60 + 76 + font10 + center + center + D8FFFFFF + + + + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 2 + 21 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + 90 + 0 + + 0 + 0 + 723 + 76 + font10 + left + center + FFFFFFFF + + + + + 730 + 0 + 200 + 76 + font10 + right + center + D8FFFFFF + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 73 + 930 + 2 + script.plex/white-square.png + 40000000 + + + + + !String.IsEmpty(ListItem.Property(is.header)) + 120 + 24 + 400 + 76 + font10 + left + center + FFFFFFFF + + + + + + + + + String.IsEmpty(ListItem.Property(is.header)) + + !Control.HasFocus(101) + 120 + 24 + + !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + -10 + 0 + 60 + 76 + font10 + center + center + D8FFFFFF + + + + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 2 + 21 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + 90 + 0 + + 0 + 0 + 723 + 76 + font10 + left + center + FFFFFFFF + + + + + 730 + 0 + 200 + 76 + font10 + right + center + D8FFFFFF + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 73 + 930 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1130 + 156 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 1044 + 76 + script.plex/white-square-rounded.png + FFE5A00D + + + !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 48 + 0 + 50 + 76 + font12 + left + center + B8000000 + + + + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 36 + 21 + 35 + 35 + script.plex/indicators/playing-circle.png + FF000000 + + + 140 + 0 + + 0 + 0 + 738 + 76 + font12 + left + center + DF000000 + + + + + 796 + 0 + 200 + 76 + font12 + right + center + B8000000 + + + + + + + !String.IsEmpty(ListItem.Property(is.header)) + 120 + 24 + 400 + 76 + font10 + left + center + FFFFFFFF + + + + + + + + 1128 + 33 + 10 + 874 + 101 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + + + 201 + 0 + 0 + 1920 + 135 + + ControlGroup(200).HasFocus(0) + false + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-artist.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-artist.xml new file mode 100644 index 000000000..797137b02 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-artist.xml @@ -0,0 +1,527 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + 0 + 135 + 101 + + + VisibleChange + 301 + 594 + 418 + 600 + 145 + 200 + 101 + -50 + horizontal + center + 200 + true + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/info-focus.png + script.plex/buttons/info.png + + + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + 60 + 0 + 1920 + 200 + + 0 + 0 + 519 + 519 + $INFO[Window.Property(thumb)] + scale + + + 579 + 5 + 1190 + 40 + font13 + left + center + FFFFFFFF + + + + 579 + 55 + 1190 + 40 + font13 + left + center + FFDDDDDD + + + + 579 + 158 + 1221 + 250 + font13 + left + FFDDDDDD + + 152 + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 0 + 585 + 1920 + 360 + + 0 + 0 + 1920 + 360 + script.plex/white-square.png + 20000000 + + + 0 + -20 + 1920 + 700 + 300 + 200 + horizontal + 2 + + + + 60 + 60 + + 0 + 0 + + 0 + 0 + 215 + 215 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 215 + 215 + $INFO[ListItem.Thumb] + scale + + + 0 + 220 + + 0 + 0 + 215 + 30 + font10 + center + FFFFFFFF + + + + 0 + 30 + 215 + 30 + font10 + center + FFFFFFFF + + + + + + + + + + + 60 + 60 + + Focus + UnFocus + + 0 + 0 + + Control.HasFocus(101) + -40 + -40 + 295 + 295 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 215 + 215 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 215 + 215 + $INFO[ListItem.Thumb] + scale + + + 0 + 220 + + false + 0 + 0 + 215 + 30 + font10 + center + FFFFFFFF + + + + 0 + 30 + 215 + 30 + font10 + center + FFFFFFFF + + + + + + Control.HasFocus(101) + -5 + -5 + 225 + 225 + script.plex/home/selected.png + + + + + + + + + + + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + false + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-background.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-background.xml new file mode 100644 index 000000000..7d28bf2fb --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-background.xml @@ -0,0 +1,65 @@ + + + 100 + + 1 + 0 + 0 + + 0xff111111 + + + !String.IsEmpty(Window(10000).Property(script.plex.background.splash)) + 710 + 459 + 500 + 162 + script.plex/splash.png + + + + !String.IsEmpty(Window(10000).Property(script.plex.background.busy)) + + 812 + 135 + 300 + 97 + script.plex/user_select/plex.png + + + 840 + 465 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 915 + 521 + 90 + 38 + script.plex/busy.gif + + + + + !String.IsEmpty(Window(10000).Property(script.plex.background.shutdown)) + + 840 + 465 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 915 + 521 + 90 + 38 + script.plex/busy.gif + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-blank.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-blank.xml new file mode 100644 index 000000000..2f887866c --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-blank.xml @@ -0,0 +1,12 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-busy.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-busy.xml new file mode 100644 index 000000000..31b167f2b --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-busy.xml @@ -0,0 +1,28 @@ + + + + 1 + 0 + 0 + + + + WindowOpen + + 840 + 465 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 915 + 521 + 90 + 38 + script.plex/busy.gif + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-busy_msg.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-busy_msg.xml new file mode 100644 index 000000000..5fc2a6bb5 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-busy_msg.xml @@ -0,0 +1,35 @@ + + + + 1 + 0 + 0 + + + + 0 + 0 + WindowOpen + + 840 + 465 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 840 + 465 + 240 + 150 + center + center + FFFFFFFF + + font14 + black + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-dropdown.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-dropdown.xml new file mode 100644 index 000000000..8ace797b8 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-dropdown.xml @@ -0,0 +1,201 @@ + + SetProperty(dropdown,1) + 100 + + 1 + 0 + 0 + + 100 + + + + 0 + 0 + 1920 + 1080 + - + - + + + 250 + !String.IsEmpty(Window.Property(show)) + 0 + 0 + + -40 + -40 + 380 + 146 + script.plex/drop-shadow.png + + + 0 + 0 + 300 + 924 + Close + noop + Close + Close + Close + noop + 200 + vertical + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square-rounded.png + + + String.IsEmpty(ListItem.Property(with.indicator)) + 0 + 0 + 300 + 66 + font12 + center + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(with.indicator)) + + 20 + 0 + 280 + 66 + font12 + left + center + FFFFFFFF + + + + 254 + 20 + 26 + 26 + $INFO[ListItem.Thumb] + keep + + + + !String.IsEmpty(ListItem.Property(separator)) + 0 + 64 + 300 + 2 + script.plex/white-square.png + + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square-rounded.png + + + String.IsEmpty(ListItem.Property(with.indicator)) + 0 + 0 + 300 + 66 + font12 + center + center + FF000000 + + + + !String.IsEmpty(ListItem.Property(with.indicator)) + + 20 + 0 + 280 + 66 + font12 + left + center + FF000000 + + + + 254 + 20 + 26 + 26 + $INFO[ListItem.Thumb] + keep + + + + !String.IsEmpty(ListItem.Property(separator)) + 0 + 64 + 300 + 2 + script.plex/white-square.png + + + + + + \ No newline at end of file diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-dropdown_header.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-dropdown_header.xml new file mode 100644 index 000000000..3c623d851 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-dropdown_header.xml @@ -0,0 +1,247 @@ + + SetProperty(dropdown,1) + 100 + + 1 + 0 + 0 + + + + + 0 + 0 + 1920 + 1080 + - + - + + + 250 + !String.IsEmpty(Window.Property(show)) + 0 + 0 + + -60 + -106 + 720 + 146 + script.plex/drop-shadow.png + + + !String.IsEmpty(Window.Property(header)) + -20 + -66 + + 0 + 0 + 640 + 132 + script.plex/white-square-rounded.png + + + 20 + 0 + 600 + 66 + font12 + center + center + FFFFFFFF + + + + + 0 + 0 + 600 + 924 + Close + noop + Close + Close + Close + noop + 200 + vertical + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 600 + 66 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 600 + 66 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 600 + 66 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 600 + 66 + script.plex/white-square-rounded.png + + + String.IsEmpty(ListItem.Property(with.indicator)) + String.IsEqual(ListItem.Property(align),center) + 0 + 0 + 600 + 66 + font12 + center + center + FFFFFFFF + + + + String.IsEmpty(ListItem.Property(with.indicator)) + String.IsEqual(ListItem.Property(align),left) + 20 + 0 + 600 + 66 + font12 + left + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(with.indicator)) + + 60 + 0 + 520 + 66 + font12 + left + center + FFFFFFFF + + + + 20 + 20 + 26 + 26 + $INFO[ListItem.Thumb] + keep + + + + !String.IsEmpty(ListItem.Property(separator)) + 0 + 64 + 600 + 2 + script.plex/white-square.png + + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 600 + 66 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 600 + 66 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 600 + 66 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 600 + 66 + script.plex/white-square-rounded.png + + + String.IsEmpty(ListItem.Property(with.indicator)) + String.IsEqual(ListItem.Property(align),center) + 0 + 0 + 600 + 66 + font12 + center + center + FF000000 + + + + String.IsEmpty(ListItem.Property(with.indicator)) + String.IsEqual(ListItem.Property(align),left) + 20 + 0 + 600 + 66 + font12 + left + center + FF000000 + + + + !String.IsEmpty(ListItem.Property(with.indicator)) + + 60 + 0 + 520 + 66 + font12 + left + center + FF000000 + + + + 20 + 20 + 26 + 26 + $INFO[ListItem.Thumb] + keep + + + + !String.IsEmpty(ListItem.Property(separator)) + 0 + 64 + 600 + 2 + script.plex/white-square.png + + + + + + \ No newline at end of file diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml new file mode 100644 index 000000000..47428c701 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml @@ -0,0 +1,1820 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + + + + + + + + + + + + + + + + + + 0 + 155 + 101 + + + VisibleChange + !String.IsEmpty(Window.Property(initialized)) + 301 + 30 + 405 + 717 + 200 + 200 + 400 + center + -50 + horizontal + 200 + true + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/info-focus.png + script.plex/buttons/info.png + + + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + !String.IsEmpty(Container(400).ListItem.Property(media.multiple)) + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/media-focus.png + script.plex/buttons/media.png + + + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/settings-focus.png + script.plex/buttons/settings.png + + + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + + 0 + 0 + 1920 + 600 + + + 60 + 0 + 657 + 393 + script.plex/thumb_fallbacks/show.png + WindowOpen + scale + + + 60 + 0 + 657 + 393 + $INFO[Container(400).ListItem.Thumb] + scale + + + !String.IsEmpty(Container(400).ListItem.Property(unwatched)) + 682 + 0 + 35 + 35 + script.plex/indicators/unwatched.png + + + + + 776 + 0 + 1360 + 60 + left + 0 + horizontal + true + + auto + 60 + font13 + left + top + FFFFFFFF + + + + !String.IsEmpty(Container(400).ListItem.Property(remainingTime)) + 10 + 6 + auto + 34 + font12 + center + center + FFE5A00D + FFE5A00D + 15 + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + + + + + 776 + 50 + 1360 + 60 + font13 + left + top + FFFFFFFF + + + + 776 + 140 + 1360 + 30 + left + 0 + horizontal + true + + auto + 30 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Container(400).ListItem.Property(unavailable)) + 10 + auto + 30 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + + + + + + 776 + 188 + 1360 + 34 + left + 0 + horizontal + true + + auto + 34 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Container(400).ListItem.Property(rating.stars)) + auto + 34 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Container(400).ListItem.Property(rating.stars)) + 6 + 134 + 22 + script.plex/stars/$INFO[Container(400).ListItem.Property(rating.stars)].png + + + !String.IsEmpty(Container(400).ListItem.Property(video.res)) + 10 + auto + 34 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + + + !String.IsEmpty(Container(400).ListItem.Property(rating)) + 1560 + 140 + 300 + 32 + right + 15 + horizontal + true + + 2 + 63 + 30 + $INFO[Container(400).ListItem.Property(rating.image)] + keep + + + auto + 30 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Container(400).ListItem.Property(rating2)) + 2 + 40 + 30 + $INFO[Container(400).ListItem.Property(rating2.image)] + keep + + + !String.IsEmpty(Container(400).ListItem.Property(rating2)) + auto + 30 + font12 + left + FFFFFFFF + + + + + + 776 + 263 + 1360 + 34 + left + 15 + horizontal + true + + !String.IsEmpty(Container(400).ListItem.Property(audio)) + auto + 34 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + auto + 34 + font12 + left + center + FFFFFFFF + + + + !String.IsEmpty(Container(400).ListItem.Property(subtitles)) + 34 + auto + 34 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + !String.IsEmpty(Container(400).ListItem.Property(subtitles)) + auto + 34 + font12 + left + center + FFFFFFFF + + + + + 776 + 340 + 1084 + 172 + font12 + left + FFFFFFFF + + + + + WindowOpen + -1 + 557 + 1 + 8 + script.plex/white-square.png + FFCC7B19 + + + !Control.IsVisible(500) + 0 + 565 + 1920 + 2 + script.plex/white-square.png + A0000000 + + + + + !String.IsEmpty(Window.Property(initialized)) + 0 + 565 + 1920 + 1800 + + 300 + 0 + + + Integer.IsGreater(Container(400).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 360 + 1920 + + 0 + 0 + 1920 + 360 + script.plex/white-square.png + 40000000 + + + 60 + 0 + 800 + 80 + font12 + left + center + AAFFFFFF + + + + 0 + 18 + 1920 + 430 + 300 + 401 + false + false + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 158 + + 0 + 0 + 299 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 299 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 264 + 0 + 35 + 35 + script.plex/indicators/unwatched.png + + + false + 0 + 175 + 299 + 60 + font12 + center + AAFFFFFF + + + + false + 0 + 210 + 299 + 60 + font12 + center + AAFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 299 + 168 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 85.5 + 20 + 128 + 128 + script.plex/home/busy.gif + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(400) + -40 + -40 + 389 + 258 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 158 + + 0 + 0 + 299 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 299 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 264 + 0 + 35 + 35 + script.plex/indicators/unwatched.png + + + Control.HasFocus(400) + + true + 0 + 175 + 299 + 60 + font12 + center + AAFFFFFF + + + + true + 0 + 210 + 299 + 60 + font12 + center + AAFFFFFF + + + + + !Control.HasFocus(400) + + false + 0 + 175 + 299 + 60 + font12 + center + FFCC7B19 + + + + false + 0 + 210 + 299 + 60 + font12 + center + FFCC7B19 + + + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 299 + 168 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 85.5 + 20 + 128 + 128 + script.plex/home/busy.gif + + + + + Control.HasFocus(400) + 0 + 0 + 309 + 178 + script.plex/home/selected.png + + + + + + + + + + Integer.IsGreater(Container(401).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 400 + 1920 + + 60 + 20 + 800 + 40 + font12 + left + center + FFFFFFFF + + + + 0 + 50 + 1920 + 360 + 400 + 402 + 200 + horizontal + 4 + + + + 55 + 29 + + 5 + 5 + + 0 + 0 + 158 + 236 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 158 + 236 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 113 + 0 + 45 + 40 + script.plex/white-square.png + FF000000 + + + 114 + 0 + 44 + 39 + script.plex/white-square.png + FFCC7B19 + + + 114 + 0 + 44 + 39 + font10 + center + center + FF000000 + + + + + false + 0 + 240 + 158 + 54 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 29 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(401) + -40 + -40 + 248 + 326 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 158 + 236 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 158 + 236 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 113 + 0 + 45 + 40 + script.plex/white-square.png + FF000000 + + + 114 + 0 + 44 + 39 + script.plex/white-square.png + FFCC7B19 + + + 114 + 0 + 44 + 39 + font10 + center + center + FF000000 + + + + + Control.HasFocus(401) + 0 + 240 + 158 + 54 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(401) + 0 + 0 + 168 + 246 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(402).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 360 + 1920 + + !String.IsEmpty(Window.Property(divider.402)) + 60 + 0 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 800 + 80 + font12 + left + center + AAFFFFFF + + + + 0 + 18 + 1920 + 430 + 401 + 403 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 175 + 299 + 60 + font10 + center + AAFFFFFF + + + + false + 0 + 205 + 299 + 60 + font10 + center + AAFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(402) + -40 + -40 + 389 + 258 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + + Control.HasFocus(402) + 0 + 175 + 299 + 60 + font10 + center + AAFFFFFF + + + + Control.HasFocus(402) + 0 + 205 + 299 + 60 + font10 + center + AAFFFFFF + + + + + + Control.HasFocus(402) + 0 + 0 + 309 + 178 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(403).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 402 + 1920 + 520 + + !String.IsEmpty(Window.Property(divider.403)) + 60 + 0 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 16 + 1920 + 520 + 402 + 404 + false + false + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + Control.HasFocus(403) + -40 + -40 + 324 + 441 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(403) + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(403) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(404).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 403 + 1920 + 410 + + !String.IsEmpty(Window.Property(divider.404)) + 60 + 20 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 20 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 36 + 1920 + 410 + 403 + 404 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + + false + 0 + 0 + 244 + 60 + font10 + center + AAFFFFFF + + + + false + 0 + 30 + 244 + 60 + font10 + center + AAFFFFFF + + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(404) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + + Control.HasFocus(404) + 0 + 0 + 244 + 60 + font10 + center + AAFFFFFF + + + + Control.HasFocus(404) + 0 + 30 + 244 + 60 + font10 + center + AAFFFFFF + + + + + + Control.HasFocus(404) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + + + Conditional + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + !String.IsEmpty(Window.Property(on.extras)) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml new file mode 100644 index 000000000..e5431162c --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml @@ -0,0 +1,8622 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 101 + 0 + 96 + 2130 + 5540 + 20 + vertical + true + 200 + + 2130 + 200 + + Conditional + Conditional + -300 + 0 + 2430 + 240 + 101 + 203 + 204 + 400 + 200 + horizontal + 4 + 3 + 102 + + + + !String.IsEmpty(ListItem.Property(item)) + 60 + 40 + 298 + 200 + + 0 + 0 + 238 + 117 + + String.IsEmpty(ListItem.Property(is.home)) + 0 + 0 + 238 + 117 + script.plex/white-square-rounded.png + FF1F1F1F + + + + String.IsEmpty(ListItem.Property(is.home)) + 84 + 23.5 + 70 + 70 + $INFO[ListItem.Thumb] + keep + + + !String.IsEmpty(ListItem.Property(is.home)) + 74 + 13.5 + 90 + 90 + script.plex/home/type/home.png + keep + + + + + false + 0 + 125 + 238 + 35 + font10 + center + FFFFFFFF + + + + + + + + + !String.IsEmpty(ListItem.Property(item)) + 60 + 40 + + Focus + UnFocus + 0 + 0 + + String.IsEmpty(ListItem.Property(is.home)) + + Control.HasFocus(101) + + -40 + -40 + 318 + 197 + script.plex/drop-shadow.png + + + UnFocus + 0 + 0 + 238 + 117 + script.plex/white-square-rounded.png + FF1F1F1F + + + UnFocus + 0 + 0 + 238 + 117 + script.plex/white-square-rounded.png + FFE5A00D + + + + !Control.HasFocus(101) + 0 + 0 + 238 + 117 + script.plex/white-square-rounded.png + FFCC7B19 + + + 84 + 23.5 + 70 + 70 + $INFO[ListItem.Thumb] + keep + + + + !String.IsEmpty(ListItem.Property(is.home)) + 74 + 13.5 + + 0 + 0 + 90 + 90 + script.plex/home/type/home.png + keep + + + Control.HasFocus(101) + UnFocus + -40 + -40 + 170 + 170 + script.plex/home/type/home-selected.png + keep + + + + Control.HasFocus(100) + 0 + 125 + 238 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + Integer.IsGreater(Container(400).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1920 + 440 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1920 + 440 + 101 + 401 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.400)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(400) + -40 + -40 + 622 + 389 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(400) + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + Control.HasFocus(400) + !String.IsEmpty(Window.Property(hub.text2lines.400)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(400) + 0 + 0 + 542 + 309 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(400).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.400)) + 10 + 12 + + + + Integer.IsGreater(Container(401).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 401 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 400 + 402 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.401)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(401) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(401) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(401) + !String.IsEmpty(Window.Property(hub.text2lines.401)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(401) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(401).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.401)) + 10 + 7 + + + + Integer.IsGreater(Container(402).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 402 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 401 + 403 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.402)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(402) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(402) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(402) + !String.IsEmpty(Window.Property(hub.text2lines.402)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(402) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(402).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.402)) + 10 + 7 + + + + Integer.IsGreater(Container(403).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 403 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 402 + 404 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.403)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(403) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(403) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(403) + !String.IsEmpty(Window.Property(hub.text2lines.403)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(403) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(403).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.403)) + 10 + 7 + + + + Integer.IsGreater(Container(404).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 404 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 403 + 405 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.404)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(404) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(404) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(404) + !String.IsEmpty(Window.Property(hub.text2lines.404)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(404) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(404).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.404)) + 10 + 7 + + + + Integer.IsGreater(Container(405).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 405 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 404 + 406 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.405)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(405) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(405) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(405) + !String.IsEmpty(Window.Property(hub.text2lines.405)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(405) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(405).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.405)) + 10 + 7 + + + + Integer.IsGreater(Container(406).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1920 + 440 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1920 + 440 + 405 + 407 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.406)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(406) + -40 + -40 + 622 + 389 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(406) + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + Control.HasFocus(406) + !String.IsEmpty(Window.Property(hub.text2lines.406)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(406) + 0 + 0 + 542 + 309 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(406).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.406)) + 10 + 12 + + + + Integer.IsGreater(Container(407).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 407 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 406 + 408 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.407)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(407) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(407) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(407) + !String.IsEmpty(Window.Property(hub.text2lines.407)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(407) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(407).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.407)) + 10 + 7 + + + + Integer.IsGreater(Container(408).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 408 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 407 + 409 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.408)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(408) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(408) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(408) + !String.IsEmpty(Window.Property(hub.text2lines.408)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(408) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(408).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.408)) + 10 + 7 + + + + Integer.IsGreater(Container(409).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 409 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 408 + 410 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.409)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(409) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(409) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(409) + !String.IsEmpty(Window.Property(hub.text2lines.409)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(409) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(409).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.409)) + 10 + 7 + + + + Integer.IsGreater(Container(410).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 410 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 409 + 411 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.410)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(410) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(410) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(410) + !String.IsEmpty(Window.Property(hub.text2lines.410)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(410) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(410).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.410)) + 10 + 7 + + + + Integer.IsGreater(Container(411).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 411 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 410 + 412 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.411)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(411) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(411) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(411) + !String.IsEmpty(Window.Property(hub.text2lines.411)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(411) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(411).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.411)) + 10 + 7 + + + + Integer.IsGreater(Container(412).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 412 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 411 + 420 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.412)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(412) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(412) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(412) + !String.IsEmpty(Window.Property(hub.text2lines.412)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(412) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(412).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.412)) + 10 + 7 + + + + Integer.IsGreater(Container(420).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 420 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 412 + 421 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.420)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(420) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(420) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(420) + !String.IsEmpty(Window.Property(hub.text2lines.420)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(420) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(420).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.420)) + 10 + 7 + + + + Integer.IsGreater(Container(421).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 421 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 420 + 422 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.421)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(421) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(421) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(421) + !String.IsEmpty(Window.Property(hub.text2lines.421)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(421) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(421).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.421)) + 10 + 7 + + + + Integer.IsGreater(Container(422).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 422 + 1920 + 392 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 392 + 421 + 413 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.422)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(422) + -40 + -40 + 324 + 324 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 244 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 72 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 58 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 234 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(422) + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(422) + !String.IsEmpty(Window.Property(hub.text2lines.422)) + 0 + 281 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(422) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(422).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.422)) + 10 + 7 + + + + Integer.IsGreater(Container(413).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 413 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 422 + 414 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.413)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(413) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(413) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(413) + !String.IsEmpty(Window.Property(hub.text2lines.413)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(413) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(413).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.413)) + 10 + 7 + + + + Integer.IsGreater(Container(414).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 414 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 413 + 415 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.414)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(414) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(414) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(414) + !String.IsEmpty(Window.Property(hub.text2lines.414)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(414) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(414).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.414)) + 10 + 7 + + + + Integer.IsGreater(Container(415).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 415 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 414 + 416 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.415)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(415) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(415) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(415) + !String.IsEmpty(Window.Property(hub.text2lines.415)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(415) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(415).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.415)) + 10 + 7 + + + + Integer.IsGreater(Container(416).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 416 + 1920 + 515 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 29 + 1920 + 515 + 415 + 417 + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + false + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.416)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(416) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + Control.HasFocus(416) + 0 + 371 + 244 + 35 + font10 + center + FFFFFFFF + + + + Control.HasFocus(416) + !String.IsEmpty(Window.Property(hub.text2lines.416)) + 0 + 398 + 244 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + Control.HasFocus(416) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(416).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.416)) + 10 + 7 + + + + Integer.IsGreater(Container(417).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1920 + 440 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1920 + 440 + 416 + 418 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.417)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(417) + -40 + -40 + 622 + 389 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(417) + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + Control.HasFocus(417) + !String.IsEmpty(Window.Property(hub.text2lines.417)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(417) + 0 + 0 + 542 + 309 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(417).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.417)) + 10 + 12 + + + + Integer.IsGreater(Container(418).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1920 + 440 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1920 + 440 + 417 + 419 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.418)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(418) + -40 + -40 + 622 + 389 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(418) + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + Control.HasFocus(418) + !String.IsEmpty(Window.Property(hub.text2lines.418)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(418) + 0 + 0 + 542 + 309 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(418).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.418)) + 10 + 12 + + + + Integer.IsGreater(Container(419).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1920 + 440 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1920 + 440 + 418 + 423 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.419)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(419) + -40 + -40 + 622 + 389 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(419) + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + Control.HasFocus(419) + !String.IsEmpty(Window.Property(hub.text2lines.419)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(419) + 0 + 0 + 542 + 309 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(419).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.419)) + 10 + 12 + + + + Integer.IsGreater(Container(423).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1920 + 440 + + 60 + 12 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1920 + 440 + 419 + 423 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + false + !String.IsEmpty(Window.Property(hub.text2lines.423)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(423) + -40 + -40 + 622 + 389 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.end)) + + 0 + 0 + 532 + 299 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + 235.5 + 99.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 202 + 85.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 532 + 299 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 289 + + 0 + 0 + 532 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 532 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 484 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 481 + 0 + + 0 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 1 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 1 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(423) + 0 + 309 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + Control.HasFocus(423) + !String.IsEmpty(Window.Property(hub.text2lines.423)) + 0 + 337 + 532 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(423) + 0 + 0 + 542 + 309 + script.plex/home/selected.png + + + + + + + + Integer.IsGreater(Container(423).NumItems,0) + !String.IsEmpty(Window.Property(hub.text2lines.423)) + 10 + 12 + + + + + 1920 + 100 + font12 + left + center + 00FFFFFF + + + + + + + + Conditional + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + !String.IsEmpty(Window(10000).Property(script.plex.off.sections)) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + String.IsEmpty(Window.Property(search.dialog)) + + Focus + UnFocus + 60 + 47.5 + 40 + 40 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + 160 + 35 + 500 + 65 + font12 + left + center + FFFFFFFF + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 203 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + 576 + 34 + 1000 + 1046 + + 0 + 0 + 1000 + 1046 + 50 + 204 + right + 0 + horizontal + 200 + true + + auto + 66 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + SetFocus(260) + + + + -93 + 93 + 66 + + 6 + 14 + 40 + 39 + $INFO[Window.Property(server.icon)] + + + 0 + 38 + 16 + 15 + $INFO[Window.Property(server.iconmod)] + + + + !String.IsEmpty(Window.Property(server.iconmod)) + 0 + 20 + 16 + 14 + $INFO[Window.Property(server.iconmod2)] + FFEEEEEE + + + + String.IsEmpty(Window.Property(server.iconmod)) + 0 + 38 + 16 + 14 + $INFO[Window.Property(server.iconmod2)] + FFEEEEEE + + + !Control.HasFocus(201) + 59 + 27 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + 99FFFFFF + + + Control.HasFocus(201) + 59 + 27 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + FF222222 + + + Control.HasFocus(260) | !String.IsEmpty(Window.Property(show.servers)) + -413 + 70 + + -40 + -40 + 580 + 146 + script.plex/drop-shadow.png + + + 432 + -13 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + FF1F1F1F + + + + 0 + 0 + 500 + 900 + 201 + 203 + 202 + SetProperty(show.servers,) + 200 + vertical + 261 + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 500 + 100 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 500 + 100 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 500 + 100 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 500 + 100 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Label2) + + 20 + 20 + 400 + 35 + font12 + left + center + FFFFFFFF + + + + 20 + 50 + 400 + 35 + font12 + left + center + FFA0A0A0 + + + + + String.IsEmpty(ListItem.Label2) + 20 + 0 + 400 + 100 + font12 + left + center + FFFFFFFF + + + + + + String.IsEmpty(ListItem.Property(status)) + String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 456 + 38 + 24 + 21 + script.plex/home/device/home.png + + + + String.IsEmpty(ListItem.Property(status)) + !String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 415 + 38 + 24 + 21 + script.plex/home/device/home.png + + + + !String.IsEmpty(ListItem.Property(status)) + String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 415 + 38 + 24 + 21 + script.plex/home/device/home.png + + + + !String.IsEmpty(ListItem.Property(status)) + !String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 374 + 38 + 24 + 21 + script.plex/home/device/home.png + + + + !String.IsEmpty(ListItem.Property(status)) + String.IsEmpty(ListItem.Property(current)) + 456 + 38 + 24 + 24 + script.plex/home/device/$INFO[ListItem.Property(status)] + + + + !String.IsEmpty(ListItem.Property(status)) + !String.IsEmpty(ListItem.Property(current)) + 415 + 38 + 24 + 24 + script.plex/home/device/$INFO[ListItem.Property(status)] + + + !String.IsEmpty(ListItem.Property(current)) + 449 + 38 + 31 + 24 + script.plex/home/device/check.png + + + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 500 + 100 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 500 + 100 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 500 + 100 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 500 + 100 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Label2) + + 20 + 20 + 400 + 35 + font12 + left + center + FF000000 + + + + 20 + 50 + 400 + 35 + font12 + left + center + FFFFFFFF + + + + + String.IsEmpty(ListItem.Label2) + 20 + 0 + 400 + 100 + font12 + left + center + FF000000 + + + + + + String.IsEmpty(ListItem.Property(status)) + String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 456 + 38 + 24 + 21 + script.plex/home/device/home.png + + + + String.IsEmpty(ListItem.Property(status)) + !String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 415 + 38 + 24 + 21 + script.plex/home/device/home.png + + + + !String.IsEmpty(ListItem.Property(status)) + String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 415 + 38 + 24 + 21 + script.plex/home/device/home.png + + + + !String.IsEmpty(ListItem.Property(status)) + !String.IsEmpty(ListItem.Property(current)) + !String.IsEmpty(ListItem.Property(local)) + 374 + 38 + 24 + 21 + script.plex/home/device/home.png + + + !String.IsEmpty(ListItem.Property(status)) + String.IsEmpty(ListItem.Property(current)) + 456 + 38 + 24 + 24 + script.plex/home/device/focus-$INFO[ListItem.Property(status)] + + + !String.IsEmpty(ListItem.Property(status)) + !String.IsEmpty(ListItem.Property(current)) + 415 + 38 + 24 + 24 + script.plex/home/device/focus-$INFO[ListItem.Property(status)] + + + !String.IsEmpty(ListItem.Property(current)) + 449 + 38 + 31 + 24 + script.plex/home/device/check.png + + + + + + + 492 + 20 + 8 + 860 + - + script.plex/white-square.png + script.plex/white-square.png + - + - + false + vertical + false + 250 + + + + + + auto + 66 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + SetFocus(250) + + + -87 + 87 + 66 + + 0 + 14 + 40 + 39 + $INFO[Window.Property(user.avatar)] + + + String.IsEmpty(Window.Property(user.avatar)) + 0 + 14 + 40 + 39 + font10 + center + center + FFFFFFFF + + + + !Control.HasFocus(202) + 53 + 27 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + 99FFFFFF + + + Control.HasFocus(202) + 53 + 27 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + FF222222 + + + Control.HasFocus(250) | !String.IsEmpty(Window.Property(show.options)) + -213 + 70 + + -40 + -40 + 380 + 146 + script.plex/drop-shadow.png + + + 226 + -13 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + FF1F1F1F + + + + 0 + 0 + 300 + 198 + 202 + noop + 201 + SetProperty(show.options,) + 200 + vertical + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square-rounded.png + + + 0 + 0 + 300 + 66 + font12 + center + center + FFFFFFFF + + + + + + !String.IsEmpty(ListItem.Property(first)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + String.IsEmpty(ListItem.Property(first)) + String.IsEmpty(ListItem.Property(last)) + String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square.png + + + !String.IsEmpty(ListItem.Property(last)) + 0 + 0 + 300 + 66 + script.plex/white-square-top-rounded.png + + + !String.IsEmpty(ListItem.Property(only)) + 0 + 0 + 300 + 66 + script.plex/white-square-rounded.png + + + 0 + 0 + 300 + 66 + font12 + center + center + FF000000 + + + + + + + + + 40 + 10 + - + + + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + String.IsEmpty(Window.Property(busy)) + !String.IsEmpty(Window.Property(no.content)) + 0 + 465 + + false + 60 + 0 + 1800 + 35 + font13 + center + FFFFFFFF + + + + false + 60 + 60 + 1800 + 35 + font13 + center + FFCCCCCC + + + + + + !String.IsEmpty(Window.Property(busy)) + Visible + 840 + 465 + + 0 + 0 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 75 + 56 + 90 + 38 + script.plex/busy.gif + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-info.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-info.xml new file mode 100644 index 000000000..9c07ff9b2 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-info.xml @@ -0,0 +1,281 @@ + + + 152 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + 0 + 135 + + + 60 + 0 + + !String.IsEmpty(Window.Property(is.poster)) + + 0 + 0 + 519 + 769 + $INFO[Window.Property(thumb.fallback)] + scale + + + 0 + 0 + 519 + 769 + $INFO[Window.Property(thumb)] + scale + + + + !String.IsEmpty(Window.Property(is.square)) + + 0 + 0 + 519 + 519 + $INFO[Window.Property(thumb.fallback)] + keep + + + 0 + 0 + 519 + 519 + $INFO[Window.Property(thumb)] + scale + + + + !String.IsEmpty(Window.Property(is.16x9)) + + 0 + 0 + 519 + 292 + $INFO[Window.Property(thumb.fallback)] + scale + + + 0 + 0 + 519 + 292 + $INFO[Window.Property(thumb)] + scale + + + + 579 + 5 + 1190 + 40 + font13 + left + center + FFFFFFFF + + + + 579 + 55 + 1190 + 40 + font13 + left + center + FFDDDDDD + + + + 579 + 157 + 152 + 1190 + 718 + font13 + left + FFDDDDDD + + + + + 1794 + 157 + 6 + 718 + true + script.plex/white-square.png + script.plex/white-square.png + script.plex/white-square.png + - + - + false + vertical + false + 204 + + + + + + 201 + 0 + 0 + 1920 + 135 + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 152 + 152 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9-chunked.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9-chunked.xml new file mode 100644 index 000000000..b209ee461 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9-chunked.xml @@ -0,0 +1,879 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + 0 + 135 + 101 + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 750 + 0 + 1170 + 1080 + + 0 + 0 + 1170 + 1080 + script.plex/white-square.png + 20000000 + + + + 0 + 0 + 1170 + 945 + 600 + 151 + 304 + 0 + vertical + 4 + 152 + 5 + + + + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -3 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + + + + + !Control.HasFocus(101) + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -2 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1085 + 156 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 1005 + 76 + script.plex/white-square-rounded.png + FFE5A00D + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 957 + 0 + 48 + 48 + script.plex/indicators/unwatched-rounded.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 933 + 15 + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 0 + 0 + 57 + 46 + script.plex/white-square-rounded.png + + + 0 + 0 + 57 + 46 + font10 + center + center + FF000000 + + + + + 60 + 0 + + 0 + 23 + 915 + 30 + font12 + left + center + DF000000 + + + + + + + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),episode) + + 1128 + 33 + 10 + 879 + 101 + 951 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + String.IsEqual(Window(10000).Property(script.plex.item.type),episode) + 1128 + 33 + 10 + 879 + + 0 + 0 + 10 + 879 + script.plex/white-square-rounded.png + + + 0 + 0 + + !Control.HasFocus(951) + String.IsEmpty(Window.Property(dragging)) + 0 + 0 + 10 + 10 + script.plex/white-square-rounded.png + + + Control.HasFocus(951) | !String.IsEmpty(Window.Property(dragging)) + 0 + 0 + 10 + 10 + script.plex/white-square-rounded.png + + + + + 0 + 0 + 10 + 879 + 152 + - + - + + + + + + + 0 + -135 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + VisibleChange + 301 + 30 + -25 + 1000 + 145 + 200 + 101 + 101 + -20 + horizontal + 200 + true + !String.IsEmpty(Window.Property(initialized)) + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.options)) | Player.HasAudio + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/chapters-focus.png + script.plex/buttons/chapters.png + + + + + + + 60 + 248 + + !String.IsEqual(Window.Property(media),show) + !String.IsEqual(Window.Property(media),movie) + 0 + 0 + 630 + 355 + 500 + $INFO[Container(101).ListItem.Property(art)] + scale + + + String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie) + 0 + 0 + 630 + 355 + 500 + $INFO[Container(101).ListItem.Property(art)] + scale + + + 0 + 355 + 440 + 80 + font12 + left + center + FFFFFFFF + + + + 630 + 355 + 180 + 80 + font12 + right + center + FFFFFFFF + + + + 0 + 435 + 630 + 2 + script.plex/white-square.png + 40000000 + + + 0 + 463 + 630 + 307 + font12 + left + FFDDDDDD + + + + + + String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 151 + 1830 + 150 + 20 + 920 + + 0 + 0 + 34 + 1050 + 100 + 152 + 200 + vertical + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + Control.HasFocus(151) + 0 + 0 + + Control.HasFocus(151) + 0 + 0 + 34 + 34 + FFE5A00D + script.plex/white-outline-rounded.png + + + + + + + + + 201 + 0 + 0 + 1920 + 135 + !String.IsEmpty(Window.Property(initialized)) + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 211 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 311 + String.IsEmpty(Window.Property(hide.filteroptions)) + 340 + 35 + 1000 + 65 + right + 30 + horizontal + 204 + 210 + 50 + + !String.IsEqual(Window.Property(media.itemType),folder) + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media),show) + !String.IsEqual(Window.Property(media),movie) + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 20 + 0 + + + + String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie) + auto + 65 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9.xml new file mode 100644 index 000000000..efd185bf1 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9.xml @@ -0,0 +1,842 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + 60 + 248 + + !String.IsEqual(Window.Property(media),show) + !String.IsEqual(Window.Property(media),movie) + 0 + 0 + 630 + 355 + 500 + $INFO[Container(101).ListItem.Property(art)] + scale + + + String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie) + 0 + 0 + 630 + 355 + 500 + $INFO[Container(101).ListItem.Property(art)] + scale + + + 0 + 355 + 440 + 80 + font12 + left + center + FFFFFFFF + + + + 630 + 355 + 180 + 80 + font12 + right + center + FFFFFFFF + + + + 0 + 435 + 630 + 2 + script.plex/white-square.png + 40000000 + + + 0 + 463 + 630 + 307 + font12 + left + FFDDDDDD + + + + + + 0 + 135 + 101 + + + VisibleChange + 301 + 30 + -25 + 1000 + 145 + 200 + 101 + 101 + -20 + horizontal + 200 + true + !String.IsEmpty(Window.Property(initialized)) + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.options)) | Player.HasAudio + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/chapters-focus.png + script.plex/buttons/chapters.png + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 750 + 0 + 1170 + 1080 + + 0 + 0 + 1170 + 1080 + script.plex/white-square.png + 20000000 + + + + 0 + 0 + 1170 + 945 + 600 + 151 + 304 + 200 + vertical + 4 + 152 + + + + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -3 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + + + + + !Control.HasFocus(101) + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -2 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1085 + 156 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 1005 + 76 + script.plex/white-square-rounded.png + FFE5A00D + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 957 + 0 + 48 + 48 + script.plex/indicators/unwatched-rounded.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 933 + 15 + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 0 + 0 + 57 + 46 + script.plex/white-square-rounded.png + + + 0 + 0 + 57 + 46 + font10 + center + center + FF000000 + + + + + 60 + 0 + + 0 + 0 + 510 + 72 + font12 + left + center + DF000000 + + + + + + + + + + + + 1128 + 33 + 10 + 879 + 101 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + + String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 151 + 1830 + 150 + 20 + 920 + + 0 + 0 + 34 + 1050 + 100 + 152 + 200 + vertical + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + Control.HasFocus(151) + 0 + 0 + + Control.HasFocus(151) + 0 + 0 + 34 + 34 + FFE5A00D + script.plex/white-outline-rounded.png + + + + + + + + + 201 + 0 + 0 + 1920 + 135 + !String.IsEmpty(Window.Property(initialized)) + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 211 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 311 + String.IsEmpty(Window.Property(hide.filteroptions)) + 340 + 35 + 1000 + 65 + right + 30 + horizontal + 204 + 210 + 50 + + !String.IsEqual(Window.Property(media.itemType),folder) + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media),show) + !String.IsEqual(Window.Property(media),movie) + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 20 + 0 + + + + String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie) + auto + 65 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square-chunked.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square-chunked.xml new file mode 100644 index 000000000..b3aab3078 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square-chunked.xml @@ -0,0 +1,943 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + 0 + 135 + 101 + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 750 + 0 + 1170 + 1080 + + 0 + 0 + 1170 + 1080 + script.plex/white-square.png + 20000000 + + + + 0 + 0 + 1170 + 945 + 600 + 151 + 304 + 0 + vertical + 4 + 152 + 5 + + + + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -3 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + + + + + !Control.HasFocus(101) + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -2 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + 0 + 0 + + String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1085 + 156 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 1005 + 76 + script.plex/white-square-rounded.png + FFE5A00D + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 957 + 0 + 48 + 48 + script.plex/indicators/unwatched-rounded.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 933 + 15 + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 0 + 0 + 57 + 46 + script.plex/white-square-rounded.png + + + 0 + 0 + 57 + 46 + font10 + center + center + FF000000 + + + + + 60 + 0 + + String.IsEmpty(ListItem.Property(is.folder)) + 0 + 23 + 885 + 30 + font12 + left + center + DF000000 + + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 23 + 885 + 30 + font10 + left + center + FF000000 + + + + + + + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),album) + + 1128 + 33 + 10 + 879 + 101 + 951 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + String.IsEqual(Window(10000).Property(script.plex.item.type),album) + 1128 + 33 + 10 + 879 + + 0 + 0 + 10 + 879 + script.plex/white-square-rounded.png + + + 0 + 0 + + !Control.HasFocus(951) + String.IsEmpty(Window.Property(dragging)) + 0 + 0 + 10 + 10 + script.plex/white-square-rounded.png + + + Control.HasFocus(951) | !String.IsEmpty(Window.Property(dragging)) + 0 + 0 + 10 + 10 + script.plex/white-square-rounded.png + + + + + 0 + 0 + 10 + 879 + 152 + - + - + + + + + + + 0 + -135 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + 301 + 30 + -25 + 1000 + 145 + 200 + 101 + 101 + -20 + horizontal + 200 + true + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.options)) | Player.HasAudio + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + String.IsEmpty(Window.Property(hide.filteroptions)) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/chapters-focus.png + script.plex/buttons/chapters.png + + + + + + + 60 + 248 + + String.IsEqual(Window.Property(media),photo) | String.IsEqual(Window.Property(media),photodirectory) + + 0 + 0 + 630 + 355 + script.plex/white-square.png + + + 0 + 0 + 630 + 355 + 500 + $INFO[Container(101).ListItem.Thumb] + keep + + + + String.IsEqual(Window.Property(media),artist) + 0 + 0 + 355 + 355 + 500 + $INFO[Container(101).ListItem.Thumb] + scale + + + !String.IsEmpty(Container(101).ListItem.Label2) + + 0 + 355 + 510 + 80 + font12 + left + center + FFFFFFFF + + + + 630 + 355 + 110 + 80 + font12 + right + center + FFFFFFFF + + + + + String.IsEmpty(Container(101).ListItem.Label2) + + 0 + 355 + 630 + 80 + font12 + left + center + FFFFFFFF + + + + + 0 + 435 + 630 + 2 + script.plex/white-square.png + 40000000 + + + 0 + 463 + 630 + 307 + font12 + left + FFDDDDDD + + + + + + String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 151 + 1830 + 150 + 20 + 920 + + 0 + 0 + 34 + 1050 + 100 + 152 + 200 + vertical + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + Control.HasFocus(151) + 0 + 0 + + Control.HasFocus(151) + 0 + 0 + 34 + 34 + FFE5A00D + script.plex/white-outline-rounded.png + + + + + + + + + 201 + 0 + 0 + 1920 + 135 + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 211 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 311 + String.IsEmpty(Window.Property(hide.filteroptions)) + 340 + 35 + 1000 + 65 + right + 30 + horizontal + 204 + 210 + 50 + + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media),artist) + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 20 + 0 + + + + String.IsEqual(Window.Property(media),artist) + auto + 65 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square.xml new file mode 100644 index 000000000..0b80694ee --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square.xml @@ -0,0 +1,908 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + 60 + 248 + + String.IsEqual(Window.Property(media),photo) | String.IsEqual(Window.Property(media),photodirectory) + + 0 + 0 + 630 + 355 + script.plex/white-square.png + + + 0 + 0 + 630 + 355 + 500 + $INFO[Container(101).ListItem.Thumb] + keep + + + + String.IsEqual(Window.Property(media),artist) + 0 + 0 + 355 + 355 + 500 + $INFO[Container(101).ListItem.Thumb] + scale + + + !String.IsEmpty(Container(101).ListItem.Label2) + + 0 + 355 + 310 + 80 + font12 + left + center + FFFFFFFF + + + + 630 + 355 + 310 + 80 + font12 + right + center + FFFFFFFF + + + + + String.IsEmpty(Container(101).ListItem.Label2) + + 0 + 355 + 630 + 80 + font12 + left + center + FFFFFFFF + + + + + 0 + 435 + 630 + 2 + script.plex/white-square.png + 40000000 + + + 0 + 463 + 630 + 307 + font12 + left + FFDDDDDD + + + + + + 0 + 135 + 101 + + + 301 + 30 + -25 + 1000 + 145 + 200 + 101 + 101 + -20 + horizontal + 200 + true + !String.IsEmpty(Window.Property(initialized)) + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.options)) | Player.HasAudio + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + String.IsEmpty(Window.Property(hide.filteroptions)) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/chapters-focus.png + script.plex/buttons/chapters.png + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 750 + 0 + 1170 + 1080 + + 0 + 0 + 1170 + 1080 + script.plex/white-square.png + 20000000 + + + + 0 + 0 + 1170 + 945 + 600 + 151 + 304 + 200 + vertical + 4 + 152 + + + + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -3 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + + + + + !Control.HasFocus(101) + 120 + 24 + + + !String.IsEmpty(ListItem.Property(unwatched)) + 880 + -2 + 35 + 35 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 861 + 14 + + 0 + 0 + 54 + 42 + script.plex/white-square-rounded.png + + + 0 + 0 + 54 + 42 + font10 + center + center + FF000000 + + + + + 0 + 0 + + 0 + 0 + + String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 915 + 72 + font10 + left + center + FFFFFFFF + + + + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 72 + 915 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1085 + 156 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 1005 + 76 + script.plex/white-square-rounded.png + FFE5A00D + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 957 + 0 + 48 + 48 + script.plex/indicators/unwatched-rounded.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 933 + 15 + + !String.IsEmpty(ListItem.Property(unwatched.count)) + 0 + 0 + 57 + 46 + script.plex/white-square-rounded.png + + + 0 + 0 + 57 + 46 + font10 + center + center + FF000000 + + + + + 60 + 0 + + String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 885 + 72 + font12 + left + center + DF000000 + + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 885 + 72 + font10 + left + center + FF000000 + + + + + + + + + + + + 1128 + 33 + 10 + 879 + 101 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + + String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 151 + 1830 + 150 + 20 + 920 + + 0 + 0 + 34 + 1050 + 100 + 152 + 200 + vertical + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + Control.HasFocus(151) + 0 + 0 + + Control.HasFocus(151) + 0 + 0 + 34 + 34 + FFE5A00D + script.plex/white-outline-rounded.png + + + + + + + + + 201 + 0 + 0 + 1920 + 135 + !String.IsEmpty(Window.Property(initialized)) + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 211 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 311 + String.IsEmpty(Window.Property(hide.filteroptions)) + 340 + 35 + 1000 + 65 + right + 30 + horizontal + 204 + 210 + 50 + + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media),artist) + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 20 + 0 + + + + String.IsEqual(Window.Property(media),artist) + auto + 65 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-music_current_playlist.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-music_current_playlist.xml new file mode 100644 index 000000000..28881da16 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-music_current_playlist.xml @@ -0,0 +1,790 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + String.IsEmpty(Window.Property(use_solid_background)) + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + false + + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Player.Art(fanart)] + + + !String.IsEmpty(Player.Art(fanart)) + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + D0404040 + + + + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Player.Art(landscape)] + + + + + 0 + 0 + 101 + + + 90 + 0 + + -15 + 75 + 669 + 669 + script.plex/white-square.png + 20FFFFFF + + + 0 + 90 + 639 + 639 + $INFO[Player.Art(thumb)] + scale + + + 0 + 764 + 639 + 35 + font12 + left + center + FFFFFFFF + + + + 0 + 799 + 639 + 35 + font12 + left + center + FFFFFFFF + + + + 0 + 834 + 639 + 35 + font12 + left + center + FFFFFFFF + + + + 0 + 869 + 639 + 35 + font12 + left + center + FFFFFFFF + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 819 + 0 + 1101 + 1080 + + 0 + 0 + 1101 + 1080 + script.plex/white-square.png + 20000000 + + + 0 + 0 + 1101 + 1080 + 200 + 152 + 411 + 200 + vertical + 4 + 152 + + + + 120 + 24 + + !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + -10 + 0 + 60 + 100 + font10 + center + center + D8FFFFFF + + + + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 2 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + 63 + 11 + 74 + 74 + $INFO[ListItem.Thumb] + scale + + + 168 + 0 + + 0 + 15 + 723 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 723 + 30 + font10 + left + center + B8FFFFFF + + + + + 661 + 0 + 200 + 100 + font10 + right + center + D8FFFFFF + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 97 + 861 + 2 + script.plex/white-square.png + 40000000 + + + + + + + + + !Control.HasFocus(101) + 120 + 24 + + !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + -10 + 0 + 60 + 100 + font10 + center + center + D8FFFFFF + + + + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 0 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + 63 + 11 + 74 + 74 + $INFO[ListItem.Thumb] + scale + + + 168 + 0 + + 0 + 15 + 723 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 723 + 30 + font10 + left + center + B8FFFFFF + + + + + 669 + 0 + 200 + 100 + font10 + right + center + D8FFFFFF + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 97 + 861 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1055 + 180 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 975 + 100 + script.plex/white-square-rounded.png + FFE5A00D + + + !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 24 + 0 + 60 + 100 + font12 + center + center + B8000000 + + + + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 36 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FF000000 + + + 103 + 0 + 100 + 100 + $INFO[ListItem.Thumb] + scale + + + 235 + 0 + + 0 + 16 + 669 + 30 + font12 + left + center + DF000000 + + + + 0 + 51 + 669 + 30 + font10 + left + center + 98000000 + + + + + 735 + 0 + 200 + 100 + font12 + right + center + B8000000 + + + + + + + + + + + 1059 + 33 + 10 + 1014 + 101 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + + 406 + + 0 + 964 + 819 + + 124 + center + 500 + 100 + -40 + horizontal + 200 + false + + Conditional + Conditional + 125 + 101 + + + 0 + 0 + 125 + 101 + 100 + 402 + 411 + font12 + - + - + + + + !Control.HasFocus(401) + + !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat.png + + + Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat.png + + + Playlist.IsRepeatOne + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-one.png + + + + Control.HasFocus(401) + + !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-focus.png + + + Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-focus.png + + + Playlist.IsRepeatOne + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-one-focus.png + + + + + + String.IsEmpty(Window.Property(pq.isremote)) + Focus + UnFocus + + 125 + 101 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + Playlist.IsRandom + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + PlayerControl(RandomOn) + PlayerControl(RandomOff) + + + + + !String.IsEmpty(Window.Property(pq.isremote)) + Conditional + Conditional + 125 + 101 + + + 0 + 0 + 125 + 101 + 100 + 404 + 401 + font12 + - + - + + + + String.IsEmpty(Window.Property(pq.shuffled)) + + !Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle.png + + + Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle-focus.png + + + + !String.IsEmpty(Window.Property(pq.shuffled)) + + !Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle.png + + + Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle-focus.png + + + + + + MusicPlayer.HasPrevious | !String.IsEmpty(Window.Property(pq.hasprevious)) + Focus + UnFocus + + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + Focus + UnFocus + + 125 + 101 + font12 + script.plex/buttons/pause-focus.png + script.plex/buttons/pause.png + Player.Paused | Player.Forwarding | Player.Rewinding + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + PlayerControl(Play) + + + + Focus + UnFocus + + 125 + 101 + font12 + script.plex/buttons/stop-focus.png + script.plex/buttons/stop.png + PlayerControl(Stop) + + + + MusicPlayer.HasNext | !String.IsEmpty(Window.Property(pq.hasnext)) + Focus + UnFocus + + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + + Focus + UnFocus + + 125 + 101 + font12 + script.plex/buttons/pqueue-focus.png + script.plex/buttons/pqueue.png + + Close + + + Focus + UnFocus + + 125 + 101 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + 0 + 940 + + Player.HasAudio + + 0 + 0 + 819 + 10 + 100 + 400 + script.plex/white-square.png + script.plex/white-square.png + A0000000 + + + Control.HasFocus(500) + Visible + 0 + 1 + 1 + 8 + script.plex/white-square.png + FFE5A00D + + + !Control.HasFocus(500) + Progressbar + 0 + 2 + 819 + 6 + script.plex/transparent-6px.png + - + script.plex/white-square-6px.png + - + - + Player.Progress + + + Control.HasFocus(500) + Progressbar + 0 + 2 + 819 + 6 + script.plex/transparent-6px.png + - + script.plex/white-square-6px.png + - + - + Player.Progress + + + + + + + + + Control.HasFocus(500) + 0 + 896 + + -50 + 0 + + Visible + 0 + 0 + 101 + 39 + script.plex/indicators/player-selection-time_box.png + D0000000 + + + 0 + 0 + 101 + 40 + font10 + center + center + FFFFFFFF + + + + + Visible + -6 + 39 + 15 + 7 + script.plex/indicators/player-selection-time_arrow.png + D0000000 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-music_player.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-music_player.xml new file mode 100644 index 000000000..18926cfc2 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-music_player.xml @@ -0,0 +1,578 @@ + + + + 1 + 0 + 0 + + 406 + + + + String.IsEmpty(Window.Property(use_solid_background)) + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + false + + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Player.Art(fanart)] + + + !String.IsEmpty(Player.Art(fanart)) + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + D0404040 + + + + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Player.Art(landscape)] + + + + + 75 + 75 + 786 + 786 + script.plex/white-square.png + 20FFFFFF + + + 90 + 90 + 756 + 756 + $INFO[Player.Art(thumb)] + + + + 939 + 0 + + 0 + 305 + 1000 + 54 + font13 + left + center + FFFFFFFF + + + + 0 + 359 + 1000 + 54 + font13 + left + center + FFFFFFFF + + + + 0 + 470 + 1000 + 54 + font13 + left + center + FFFFFFFF + + + + 0 + 580 + 1000 + 54 + font13 + left + center + FFFFFFFF + + + + + + 1845 + 0 + + 0 + 738 + 1000 + 54 + font13 + right + center + 80FFFFFF + + + + 0 + 794 + 1000 + 54 + font13 + right + center + 80FFFFFF + + + + + + + + 406 + + 360 + 964 + 1200 + + 124 + center + 100 + -40 + horizontal + 200 + true + + Conditional + Conditional + 125 + 101 + + + 0 + 0 + 125 + 101 + 100 + 402 + 411 + font12 + - + - + + + + !Control.HasFocus(401) + + !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat.png + + + Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat.png + + + Playlist.IsRepeatOne + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-one.png + + + + Control.HasFocus(401) + + !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-focus.png + + + Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-focus.png + + + Playlist.IsRepeatOne + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-one-focus.png + + + + + + String.IsEmpty(Window.Property(pq.isremote)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + Playlist.IsRandom + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + PlayerControl(RandomOn) + PlayerControl(RandomOff) + + + + + !String.IsEmpty(Window.Property(pq.isremote)) + Conditional + Conditional + 125 + 101 + + + 0 + 0 + 125 + 101 + 100 + 404 + 401 + font12 + - + - + + + + String.IsEmpty(Window.Property(pq.shuffled)) + + !Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle.png + + + Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle-focus.png + + + + !String.IsEmpty(Window.Property(pq.shuffled)) + + !Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle.png + + + Control.HasFocus(422) + 0 + 0 + 125 + 101 + script.plex/buttons/shuffle-focus.png + + + + + + MusicPlayer.HasPrevious | !String.IsEmpty(Window.Property(pq.hasprevious)) + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/pause-focus.png + script.plex/buttons/pause.png + Player.Paused | Player.Forwarding | Player.Rewinding + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + PlayerControl(Play) + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/stop-focus.png + script.plex/buttons/stop.png + PlayerControl(Stop) + + + + MusicPlayer.HasNext | !String.IsEmpty(Window.Property(pq.hasnext)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + + + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/pqueue-focus.png + script.plex/buttons/pqueue.png + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + 0 + 940 + + Player.HasAudio + + 0 + 0 + 1920 + 10 + 400 + script.plex/white-square.png + script.plex/white-square.png + A0000000 + + + Control.HasFocus(100) + Visible + 0 + 1 + 1 + 8 + script.plex/white-square.png + FFE5A00D + + + !Control.HasFocus(100) + Progressbar + 0 + 2 + 1920 + 6 + script.plex/transparent-6px.png + - + script.plex/white-square-6px.png + - + - + Player.Progress + + + Control.HasFocus(100) + Progressbar + 0 + 2 + 1920 + 6 + script.plex/transparent-6px.png + - + script.plex/white-square-6px.png + - + - + Player.Progress + + + + + + + + + Control.HasFocus(100) + 0 + 896 + + -50 + 0 + + Visible + 0 + 0 + 101 + 39 + script.plex/indicators/player-selection-time_box.png + D0000000 + + + 0 + 0 + 101 + 40 + font10 + center + center + FFFFFFFF + + + + + Visible + -6 + 39 + 15 + 7 + script.plex/indicators/player-selection-time_arrow.png + D0000000 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-options_dialog.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-options_dialog.xml new file mode 100644 index 000000000..fb660d70e --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-options_dialog.xml @@ -0,0 +1,136 @@ + + 1001 + + 1 + 0 + 0 + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + !String.IsEmpty(Window.Property(initialized)) + 585 + 360 + + -40 + -40 + 830 + 440 + script.plex/drop-shadow.png + + + 0 + 0 + 750 + 360 + script.plex/white-square-rounded.png + + + 0 + 0 + 750 + 80 + script.plex/white-square-top-rounded.png + + + + 48 + 31 + 19 + 19 + script.plex/indicators/circle-19.png + + + + 115 + 0 + 575 + 80 + font12 + left + center + FFE5A00D + + + + + 115 + 105 + 575 + 125 + font10 + left + FFFFFFFF + + + + + 1001 + -10 + 220 + 770 + 155 + right + -50 + horizontal + 0 + true + + !String.IsEmpty(Window.Property(button.0)) + Focus + UnFocus + 0 + 0 + auto + 143 + font10 + script.plex/buttons/blank-focus.png + script.plex/buttons/blank.png + 70 + FF000000 + FF000000 + + + + !String.IsEmpty(Window.Property(button.1)) + Focus + UnFocus + 0 + 0 + auto + 143 + font10 + script.plex/buttons/blank-focus.png + script.plex/buttons/blank.png + 70 + FF000000 + FF000000 + + + + !String.IsEmpty(Window.Property(button.2)) + Focus + UnFocus + 0 + 0 + auto + 143 + font10 + script.plex/buttons/blank-focus.png + script.plex/buttons/blank.png + 70 + FF000000 + FF000000 + + + + + + + \ No newline at end of file diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-photo.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-photo.xml new file mode 100644 index 000000000..071430b70 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-photo.xml @@ -0,0 +1,717 @@ + + + + 1 + 0 + 0 + + 250 + 100 + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + Conditional + Conditional + Conditional + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(photo)] + keep + + + !String.IsEmpty(Window.Property(is.updating)) + VisibleChange + + 840 + 465 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 915 + 521 + 90 + 38 + script.plex/busy.gif + + + + + 0 + 0 + 1920 + 1080 + - + - + !String.IsEmpty(Window.Property(OSD)) + - + - + + SetProperty(OSD,1) + SetFocus(400) + SetProperty(OSD,) + SetProperty(OSD,1) + SetFocus(400) + SetFocus(501) + SetFocus(400) + + + Conditional + 0 + 0 + + !String.IsEmpty(Window.Property(OSD)) + + 0 + 940 + 1920 + 140 + script.plex/white-square.png + A0000000 + + + 406 + + 360 + 964 + 1200 + + 124 + center + 250 + SetProperty(OSD,) + 250 + SetProperty(OSD,) + -40 + horizontal + 200 + true + + + String.IsEmpty(Window.Property(no.playlist)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/repeat-focus.png + script.plex/buttons/repeat.png + !String.IsEmpty(Window.Property(pq.repeat)) + script.plex/buttons/repeat-focus.png + script.plex/buttons/repeat.png + + + + false + !String.IsEmpty(Window.Property(no.playlist)) + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.playlist)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + !String.IsEmpty(Window.Property(pq.shuffled)) + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + false + !String.IsEmpty(Window.Property(no.playlist)) + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/rotate-focus.png + script.plex/buttons/rotate.png + + + + + + String.IsEmpty(Window.Property(hide.prev)) + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + false + !String.IsEmpty(Window.Property(hide.prev)) + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + !String.IsEmpty(Window.Property(playing)) + script.plex/buttons/pause-focus.png + script.plex/buttons/pause.png + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/stop-focus.png + script.plex/buttons/stop.png + + + + String.IsEmpty(Window.Property(hide.next)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + false + !String.IsEmpty(Window.Property(hide.next)) + 0 + 0 + 125 + 101 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + + + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/square2x2-focus.png + script.plex/buttons/square2x2.png + !String.IsEmpty(Window.Property(show.pqueue)) + script.plex/buttons/square2x2-focus.png + script.plex/buttons/square2x2-focus.png + SetProperty(show.pqueue,1) + SetProperty(show.pqueue,) + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/info-focus.png + script.plex/buttons/info.png + !String.IsEmpty(Window.Property(show.info)) + script.plex/buttons/info-focus.png + script.plex/buttons/info-focus.png + SetProperty(show.info,1) + SetProperty(show.info,) + + + + false + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/tags-focus.png + script.plex/buttons/tags.png + + + + false + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + + 0 + 1080 + 1920 + 135 + SetProperty(OSD,1) + 400 + font12 + - + - + + SetProperty(OSD,1) + SetFocus(400) + + + + 0 + 1080 + 1920 + 135 + script.plex/white-square.png + FF000000 + + + 0 + 1080 + 1920 + 135 + + + 0 + horizontal + 4 + 7 + + + + 0 + 0 + + 2.5 + 6 + + 0 + 0 + 123 + 123 + script.plex/thumb_fallbacks/photo.png + + + 0 + 0 + 123 + 123 + $INFO[ListItem.Thumb] + scale + + + + + + + + + 0 + 0 + + 2.5 + 6 + + 0 + 0 + 123 + 123 + script.plex/thumb_fallbacks/photo.png + + + 0 + 0 + 123 + 123 + $INFO[ListItem.Thumb] + scale + + + + + + + 892.5 + 1080 + 135 + 135 + script.plex/home/selected.png + + + + + + !String.IsEmpty(Window.Property(show.info)) + 1470 + 0 + 450 + 1080 + + 0 + 0 + 450 + 1080 + script.plex/white-square.png + 4C000000 + + + 0 + 0 + 450 + 1080 + 0 + vertical + + + + 450 + 21 + - + - + 00000000 + + + + 450 + 37 + script.plex/white-square.png + script.plex/white-square.png + 00000000 + left + center + 28 + font12 + + + + 450 + 37 + script.plex/white-square.png + script.plex/white-square.png + 00000000 + left + center + 28 + font12 + + + + + 450 + 21 + - + - + 00000000 + + + + + + 450 + 21 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + + + + !String.IsEmpty(Window.Property(camera.model)) + 450 + 37 + + 0 + 0 + 450 + 37 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + left + center + 28 + font12 + + + + 393 + 6 + 29 + 24 + script.plex/indicators/camera.png + A0FFFFFF + + + + !String.IsEmpty(Window.Property(camera.lens)) + 450 + 37 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + left + center + 28 + font12 + + + + !String.IsEmpty(Window.Property(photo.container)) + 0 + 0 + 450 + 37 + + 0 + 0 + 450 + 37 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + left + center + 28 + font12 + + + + 28 + 5 + 200 + 26 + 0 + horizontal + right + + auto + 26 + font10 + center + top + FF000000 + FF000000 + 5 + -3 + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + + + + + + + 450 + 21 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + + + + !String.IsEmpty(Window.Property(camera.settings)) + 450 + 80 + + + 0 + 0 + 450 + 1 + script.plex/indicators/info-sep.png + 99000000 + + + 28 + 0 + 394 + 1 + script.plex/white-square.png + 999B9B9B + + + + 0 + 1 + 450 + 21 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + + + + 0 + 22 + 450 + 37 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + left + center + 28 + font12 + + + + + 0 + 59 + 450 + 21 + script.plex/white-square.png + script.plex/white-square.png + 99000000 + + + + + + !String.IsEmpty(Window.Property(photo.summary)) + 450 + 152 + + 0 + 0 + 450 + 152 + script.plex/white-square.png + 661F1F1F + + + 28 + 24 + 394 + 100 + font12 + left + center + + + + + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-pin_login.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-pin_login.xml new file mode 100644 index 000000000..6dcac7f4a --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-pin_login.xml @@ -0,0 +1,82 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + !String.IsEmpty(Window.Property(pin.image.0)) + 0 + 0 + 1920 + 1080 + script.plex/sign_in/pin-display.jpg + + + String.IsEmpty(Window.Property(pin.image.0)) + String.IsEmpty(Window.Property(linking)) + 0 + 0 + 1920 + 1080 + script.plex/sign_in/generating-code.jpg + + + !String.IsEmpty(Window.Property(linking)) + 0 + 0 + 1920 + 1080 + script.plex/sign_in/linking-account.jpg + + + + 270 + 738 + 800 + 153 + WeatherTemp + left + center + FFFFFFFF + + + + + 406 + 265 + 708 + 1000 + + 200 + left + 0 + horizontal + + 200 + 200 + $INFO[Window.Property(pin.image.0)] + + + 200 + 200 + $INFO[Window.Property(pin.image.1)] + + + 200 + 200 + $INFO[Window.Property(pin.image.2)] + + + 200 + 200 + $INFO[Window.Property(pin.image.3)] + + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-playlist.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-playlist.xml new file mode 100644 index 000000000..3e00fca9c --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-playlist.xml @@ -0,0 +1,883 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + 0 + 135 + 101 + + + 60 + 0 + + 0 + 5 + 420 + 40 + font13 + left + center + FFFFFFFF + + + + 0 + 60 + 420 + 40 + font13 + left + center + FFFFFFFF + + + + 0 + 142 + 630 + 630 + $INFO[Window.Property(playlist.thumb)] + scale + + + + + VisibleChange + 303 + 50 + 784 + 650 + 145 + 200 + 101 + -50 + horizontal + center + 200 + true + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + !String.IsEmpty(Window.Property(show.options)) | Player.HasAudio + Focus + UnFocus + + 0 + 0 + 174 + 139 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 750 + 0 + 1170 + 1080 + + 0 + 0 + 1170 + 1080 + script.plex/white-square.png + 20000000 + + + 0 + 0 + 1170 + 945 + 200 + 152 + 300 + 200 + vertical + 4 + 152 + + + + 120 + 24 + + String.IsEmpty(ListItem.Property(track.ID)) | !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + -10 + 0 + 60 + 100 + font10 + center + center + D8FFFFFF + + + + !String.IsEmpty(ListItem.Property(track.ID)) + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 2 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 74 + 74 + $INFO[ListItem.Thumb] + scale + + + 168 + 0 + + 0 + 15 + 692 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 692 + 30 + font10 + left + center + B8FFFFFF + + + + + + !String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 132 + 74 + $INFO[ListItem.Thumb] + scale + + + String.IsEmpty(ListItem.Property(watched)) + 895 + -1 + 35 + 35 + script.plex/indicators/unwatched.png + + + 226 + 0 + + 0 + 15 + 584 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 584 + 30 + font10 + left + center + B8FFFFFF + + + + + + 730 + 0 + 200 + 100 + font10 + right + center + D8FFFFFF + + + + + False + 63 + 85 + + 0 + 0 + 870 + 8 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 870 + 6 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 97 + 930 + 2 + script.plex/white-square.png + 40000000 + + + + + + + + + !Control.HasFocus(101) + 120 + 24 + + String.IsEmpty(ListItem.Property(track.ID)) | !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + -10 + 0 + 60 + 100 + font10 + center + center + D8FFFFFF + + + + !String.IsEmpty(ListItem.Property(track.ID)) + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 0 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 74 + 74 + $INFO[ListItem.Thumb] + scale + + + 168 + 0 + + 0 + 15 + 692 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 692 + 30 + font10 + left + center + B8FFFFFF + + + + + + !String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 132 + 74 + $INFO[ListItem.Thumb] + scale + + + String.IsEmpty(ListItem.Property(watched)) + 895 + -1 + 35 + 35 + script.plex/indicators/unwatched.png + + + 226 + 0 + + 0 + 15 + 584 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 584 + 30 + font10 + left + center + B8FFFFFF + + + + + + 730 + 0 + 200 + 100 + font10 + right + center + D8FFFFFF + + + + !String.IsEmpty(ListItem.Property(progress)) + 63 + 88 + + 0 + 0 + 870 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 870 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 97 + 930 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1124 + 180 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 1044 + 100 + script.plex/white-square-rounded.png + FFE5A00D + + + + + String.IsEmpty(ListItem.Property(track.ID)) | !String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 24 + 0 + 60 + 100 + font12 + center + center + B8000000 + + + + !String.IsEmpty(ListItem.Property(track.ID)) + String.IsEqual(ListItem.Property(track.ID),Window(10000).Property(script.plex.track.ID)) + 36 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FF000000 + + + String.IsEmpty(ListItem.Property(video)) + + String.IsEmpty(ListItem.Property(video)) + 103 + 0 + 100 + 100 + $INFO[ListItem.Thumb] + scale + + + 235 + 0 + + 0 + 16 + 638 + 30 + font12 + left + center + DF000000 + + + + 0 + 51 + 638 + 30 + font10 + left + center + 98000000 + + + + + + !String.IsEmpty(ListItem.Property(video)) + + 103 + 0 + 178 + 100 + $INFO[ListItem.Thumb] + scale + + + String.IsEmpty(ListItem.Property(watched)) + 951 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + 313 + 0 + + 0 + 16 + 510 + 30 + font12 + left + center + DF000000 + + + + 0 + 51 + 510 + 30 + font10 + left + center + 98000000 + + + + + + 802 + 0 + 200 + 100 + font12 + right + center + B8000000 + + + + !String.IsEmpty(ListItem.Property(progress)) + 103 + 91 + + 0 + 0 + 899 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 899 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + + + + + + + + 1128 + 33 + 10 + 879 + 101 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + false + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-playlists.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-playlists.xml new file mode 100644 index 000000000..7e5e9621e --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-playlists.xml @@ -0,0 +1,567 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + 0 + 115 + 101 + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 0 + 0 + 1920 + 360 + + 60 + 0 + 1800 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1920 + 390 + 200 + 301 + 200 + horizontal + 2 + + + + 40 + 40 + + 21 + 21 + + 0 + 0 + 238 + 238 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 238 + 238 + $INFO[ListItem.Thumb] + scale + + + -30 + 248 + 298 + 40 + font10 + center + FFFFFFFF + + + + -30 + 278 + 298 + 40 + font10 + center + FFFFFFFF + + + + + + + + + + 40 + 40 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(101) + -19 + -19 + 318 + 318 + script.plex/drop-shadow.png + + + 21 + 21 + + 0 + 0 + 238 + 238 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 238 + 238 + $INFO[ListItem.Thumb] + scale + + + -30 + 248 + 298 + 40 + font10 + center + FFFFFFFF + + + + -30 + 278 + 298 + 40 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(101) + 16 + 16 + 248 + 248 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(301).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + Conditional + 301 + 0 + 445 + 1920 + 360 + + Control.IsVisible(100) + 60 + 0 + 1800 + 2 + script.plex/white-square.png + 661F1F1F + + + 60 + 0 + 1800 + 80 + font12 + left + center + FFFFFFFF + + + + -21.5 + 30 + 1941.5 + 700 + 101 + 200 + horizontal + 2 + + + + 40 + 40 + + 41.5 + 25.5 + + 0 + 0 + 537 + 303 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 537 + 303 + $INFO[ListItem.Thumb] + scale + + + -30 + 313 + 597 + 40 + font10 + center + FFFFFFFF + + + + -30 + 343 + 597 + 40 + font10 + center + FFFFFFFF + + + + + + + + + + 40 + 40 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(301) + 1.5 + -15.5 + 617 + 383 + script.plex/drop-shadow.png + + + 41.5 + 25.5 + + 0 + 0 + 537 + 303 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 537 + 303 + $INFO[ListItem.Thumb] + scale + + + -30 + 313 + 597 + 40 + font10 + center + FFFFFFFF + + + + -30 + 343 + 597 + 40 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(301) + 36.5 + 20.5 + 547 + 313 + script.plex/home/selected.png + + + + + + + + + + + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + false + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-plex_pass.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-plex_pass.xml new file mode 100644 index 000000000..d4c0be6d5 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-plex_pass.xml @@ -0,0 +1,39 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + 0 + 0 + 1920 + 1080 + script.plex/sign_in/plexpass.jpg + + + + 1436 + 802 + 275 + 100 + 200 + font13 + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-posters-small.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-posters-small.xml new file mode 100644 index 000000000..bfd43ed7d --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-posters-small.xml @@ -0,0 +1,877 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + 0 + 135 + 101 + + + VisibleChange + !Integer.IsGreater(Container(101).ListItem.Property(index),9) + String.IsEmpty(Window.Property(no.content)) + String.IsEmpty(Window.Property(no.content.filtered)) + !String.IsEmpty(Window.Property(initialized)) + 301 + 30 + -25 + 1000 + 145 + 200 + 101 + -20 + horizontal + 200 + true + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.options)) | Player.HasAudio + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/chapters-focus.png + script.plex/buttons/chapters.png + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 0 + -35 + 1920 + 1080 + + + 0 + 0 + 1800 + 1190 + 300 + 151 + 200 + vertical + 2 + 152 + + + + 55 + 137 + + 5 + 5 + + 0 + 0 + 144 + 213 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 144 + 213 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 203 + + 0 + 0 + 144 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 144 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 115 + 0 + 29 + 29 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 93 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 94 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 94 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + String.IsEmpty(ListItem.Property(subtitle)) + false + 0 + 218 + 144 + 72 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(subtitle)) + false + 0 + 218 + 144 + 72 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 137 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(101) + -40 + -40 + 234 + 316 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 144 + 213 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 144 + 213 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 203 + + 0 + 0 + 144 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 144 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 115 + 0 + 29 + 29 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 93 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 94 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 94 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + String.IsEmpty(ListItem.Property(subtitle)) + true + 0 + 218 + 144 + 72 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(subtitle)) + true + 0 + 218 + 144 + 30 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(101) + 0 + 0 + 154 + 225 + script.plex/home/selected.png + + + + + + + + + + + String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 151 + 1780 + 150 + 20 + 920 + + 0 + 0 + 34 + 1050 + 100 + 152 + 200 + vertical + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + Control.HasFocus(151) + 0 + 0 + + Control.HasFocus(151) + 0 + 0 + 34 + 34 + FFE5A00D + script.plex/white-outline-rounded.png + + + + + + + + + + 1860 + 150 + 12 + 910 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + Conditional + !String.IsEmpty(Window.Property(initialized)) + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + Integer.IsGreater(Container(101).ListItem.Property(index),9) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font10 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 211 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + String.IsEmpty(Window.Property(hide.filteroptions)) + 340 + 35 + 870 + 65 + right + 30 + horizontal + 204 + 210 + 50 + + !String.IsEqual(Window.Property(media.itemType),folder) + false + auto + 65 + font10 + A0FFFFFF + A0FFFFFF + A0FFFFFF + center + center + - + - + 0 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font10 + A0FFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + String.IsEqual(Window.Property(subDir),1) | ![String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie)] + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 20 + 0 + + + + !String.IsEqual(Window.Property(subDir),1) + [String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie)] + auto + 65 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font10 + A0FFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + + 213 + 35 + 200 + 65 + font10 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(no.content)) + 0 + 465 + + false + 60 + 0 + 1800 + 35 + font13 + center + FFFFFFFF + + + + false + 60 + 60 + 1800 + 35 + font13 + center + FFCCCCCC + + + + + + !String.IsEmpty(Window.Property(no.content.filtered)) + 0 + 465 + + false + 60 + 0 + 1800 + 35 + font13 + center + FFFFFFFF + + + + false + 60 + 60 + 1800 + 35 + font13 + center + FFCCCCCC + + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-posters.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-posters.xml new file mode 100644 index 000000000..3dd674899 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-posters.xml @@ -0,0 +1,851 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + 0 + 135 + 101 + + + VisibleChange + !Integer.IsGreater(Container(101).ListItem.Property(index),5) + String.IsEmpty(Window.Property(no.content)) + String.IsEmpty(Window.Property(no.content.filtered)) + !String.IsEmpty(Window.Property(initialized)) + 301 + 30 + -25 + 1000 + 145 + 200 + 101 + -20 + horizontal + 200 + true + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.options)) | Player.HasAudio + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/chapters-focus.png + script.plex/buttons/chapters.png + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 0 + -35 + 1920 + 1080 + + + 0 + 0 + 1800 + 1190 + 300 + 151 + 200 + vertical + 2 + 152 + + + + 55 + 137 + + 5 + 5 + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 199 + 0 + 45 + 45 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + false + 0 + 371 + 244 + 72 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 137 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(101) + -40 + -40 + 334 + 451 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 199 + 0 + 45 + 45 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + + true + 0 + 371 + 244 + 72 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(101) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + + + + String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 151 + 1780 + 150 + 20 + 920 + + 0 + 0 + 34 + 1050 + 100 + 152 + 200 + vertical + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + Control.HasFocus(151) + 0 + 0 + + Control.HasFocus(151) + 0 + 0 + 34 + 34 + FFE5A00D + script.plex/white-outline-rounded.png + + + + + + + + + + 1860 + 150 + 12 + 910 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + Conditional + 201 + 0 + 0 + 1920 + 135 + !String.IsEmpty(Window.Property(initialized)) + + VisibleChange + ControlGroup(200).HasFocus(0) + Integer.IsGreater(Container(101).ListItem.Property(index),5) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font10 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 211 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + String.IsEmpty(Window.Property(hide.filteroptions)) + 340 + 35 + 870 + 65 + right + 30 + horizontal + 204 + 210 + 50 + + !String.IsEqual(Window.Property(media.itemType),folder) + false + auto + 65 + font10 + A0FFFFFF + A0FFFFFF + A0FFFFFF + center + center + - + - + 0 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font10 + A0FFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + String.IsEqual(Window.Property(subDir),1) | ![String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie)] + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 20 + 0 + + + + !String.IsEqual(Window.Property(subDir),1) + [String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie)] + auto + 65 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media.itemType),folder) + auto + 65 + font10 + A0FFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + + 213 + 35 + 200 + 65 + font10 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(no.content)) + 0 + 465 + + false + 60 + 0 + 1800 + 35 + font13 + center + FFFFFFFF + + + + false + 60 + 60 + 1800 + 35 + font13 + center + FFCCCCCC + + + + + + !String.IsEmpty(Window.Property(no.content.filtered)) + 0 + 465 + + false + 60 + 0 + 1800 + 35 + font13 + center + FFFFFFFF + + + + false + 60 + 60 + 1800 + 35 + font13 + center + FFCCCCCC + + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-pre_play.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-pre_play.xml new file mode 100644 index 000000000..9b7c28131 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-pre_play.xml @@ -0,0 +1,1494 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + + + + + + + + + + + + + + 0 + 155 + 101 + + + VisibleChange + !String.IsEmpty(Window.Property(initialized)) + 302 + 428 + 410 + 1000 + 145 + 200 + 400 + -50 + horizontal + 200 + true + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/info-focus.png + script.plex/buttons/info.png + + + + + String.IsEmpty(Window.Property(unavailable)) + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + !String.IsEmpty(Window.Property(trailer.button)) + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/trailer-focus.png + script.plex/buttons/trailer.png + + + + !String.IsEmpty(Window.Property(media.multiple)) + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/media-focus.png + script.plex/buttons/media.png + + + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/settings-focus.png + script.plex/buttons/settings.png + + + + Focus + UnFocus + + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + 0 + 0 + 1920 + 600 + + !String.IsEmpty(Window.Property(preview.no)) + + 60 + 0 + 347 + 518 + script.plex/thumb_fallbacks/movie.png + WindowOpen + scale + + + 60 + 0 + 347 + 518 + $INFO[Window.Property(thumb)] + scale + + + !String.IsEmpty(Window.Property(unwatched)) + 359 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + + + !String.IsEmpty(Window.Property(preview.yes)) + 60 + 0 + + 0 + 0 + 347 + 315 + script.plex/thumb_fallbacks/show.png + WindowOpen + scale + + + 0 + 323 + 347 + 195 + script.plex/white-square.png + scale + + + + 0 + 0 + 347 + 315 + $INFO[Window.Property(thumb)] + scale + + + 0 + 323 + 347 + 195 + $INFO[Window.Property(preview)] + scale + + + + 466 + 0 + 1360 + 60 + left + 0 + horizontal + true + + auto + 60 + font13 + left + FFFFFFFF + + + + !String.IsEmpty(Window.Property(remainingTime)) + 10 + 6 + auto + 34 + font12 + center + center + FFE5A00D + FFE5A00D + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + + 466 + 68 + 1360 + 34 + left + 0 + horizontal + true + + auto + 34 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Window.Property(rating.stars)) + auto + 34 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Window.Property(rating.stars)) + 6 + 134 + 22 + script.plex/stars/$INFO[Window.Property(rating.stars)].png + + + !String.IsEmpty(Window.Property(video.res)) + 10 + auto + 34 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + !String.IsEmpty(Window.Property(unavailable)) + 10 + auto + 34 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + + + !String.IsEmpty(Window.Property(rating)) + 1560 + 4 + 300 + 32 + right + 15 + horizontal + true + + 2 + 63 + 30 + $INFO[Window.Property(rating.image)] + keep + + + auto + 30 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Window.Property(rating2)) + 2 + 40 + 30 + $INFO[Window.Property(rating2.image)] + keep + + + !String.IsEmpty(Window.Property(rating2)) + auto + 30 + font12 + left + FFFFFFFF + + + + + + !String.IsEmpty(Window.Property(directors)) + 466 + 130 + 1360 + 30 + font12 + left + 99FFFFFF + + + + !String.IsEmpty(Window.Property(writers)) + 466 + 165 + 1360 + 30 + font12 + left + 99FFFFFF + + + + 466 + 223 + 1360 + 34 + left + 15 + horizontal + true + + !String.IsEmpty(Window.Property(audio)) + auto + 34 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + auto + 34 + font12 + left + center + FFFFFFFF + + + + !String.IsEmpty(Window.Property(subtitles)) + 30 + auto + 34 + font12 + center + center + FFFFFFFF + FFFFFFFF + 15 + script.plex/white-square-rounded-top-padded.png + script.plex/white-square-rounded-top-padded.png + + + + !String.IsEmpty(Window.Property(subtitles)) + auto + 34 + font12 + left + center + FFFFFFFF + + + + + 466 + 290 + 1360 + 102 + font12 + left + FFFFFFFF + + + + WindowOpen + -1 + 557 + 1 + 8 + script.plex/white-square.png + FFCC7B19 + + + !Control.IsVisible(500) + 0 + 565 + 1920 + 2 + script.plex/white-square.png + A0000000 + + + + + 0 + 565 + 1920 + 1800 + + 300 + 0 + + + + Integer.IsGreater(Container(400).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 400 + 1920 + 446 + + 60 + 20 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 36 + 1920 + 410 + 300 + 401 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + + false + 0 + 0 + 244 + 60 + font10 + center + AAFFFFFF + + + + false + 0 + 30 + 244 + 60 + font10 + center + AAFFFFFF + + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(403) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + + Control.HasFocus(400) + 0 + 0 + 244 + 60 + font10 + center + AAFFFFFF + + + + Control.HasFocus(400) + 0 + 30 + 244 + 60 + font10 + center + AAFFFFFF + + + + + + Control.HasFocus(400) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + + + Integer.IsGreater(Container(401).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 401 + 1920 + 446 + + 60 + 20 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 36 + 1920 + 410 + 400 + 402 + 200 + horizontal + 4 + + + + 55 + 61 + + 0 + 0 + 520 + 310 + script.plex/white-square.png + 60000000 + + + 20 + 20 + + 0 + 0 + + 10 + -5 + 70 + 70 + script.plex/reviews/$INFO[ListItem.Thumb].png + + + false + 100 + 0 + 400 + 30 + font10 + left + DDFFFFFF + + + + false + 100 + 30 + 400 + 30 + font10 + left + 66FFFFFF + + + + + 0 + 80 + 480 + 190 + font10 + left + AAFFFFFF + + + + + + + + + + 55 + 61 + + 0 + 0 + 520 + 310 + script.plex/white-square.png + 80000000 + + + 20 + 20 + + 0 + 0 + + 10 + -5 + 70 + 70 + script.plex/reviews/$INFO[ListItem.Thumb].png + + + false + 100 + 0 + 400 + 30 + font10 + left + DDFFFFFF + + + + false + 100 + 30 + 400 + 30 + font10 + left + 66FFFFFF + + + + + 0 + 80 + 480 + 190 + font10 + left + DDFFFFFF + + Control.HasFocus(401) + + + + Control.HasFocus(401) + -5 + -5 + 530 + 320 + script.plex/home/selected.png + + + + + + + + + + Integer.IsGreater(Container(402).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 360 + 1920 + + 0 + 0 + 1920 + 360 + script.plex/white-square.png + 40000000 + + + 60 + 0 + 800 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 18 + 1920 + 430 + 401 + 403 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + 0 + 180 + 299 + 60 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(402) + -40 + -40 + 389 + 258 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + 0 + 180 + 299 + 60 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(402) + 0 + 0 + 309 + 178 + script.plex/home/selected.png + + + + + + + + + + + Integer.IsGreater(Container(403).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 403 + 1920 + 520 + + !String.IsEmpty(Window.Property(divider.403)) + 60 + 0 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 16 + 1920 + 520 + 402 + false + false + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(403) + -40 + -40 + 324 + 441 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(403) + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(403) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + + + + + + + + + + Conditional + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + !String.IsEmpty(Window.Property(on.extras)) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-pre_signin.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-pre_signin.xml new file mode 100644 index 000000000..9c8b5d391 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-pre_signin.xml @@ -0,0 +1,39 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + 0 + 0 + 1920 + 1080 + script.plex/sign_in/pre-signin.jpg + + + + 1437 + 801 + 275 + 104 + 200 + font13 + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-refresh_code.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-refresh_code.xml new file mode 100644 index 000000000..482622389 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-refresh_code.xml @@ -0,0 +1,39 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + 0 + 0 + 1920 + 1080 + script.plex/sign_in/refresh-code.jpg + + + + 1383 + 802 + 335 + 102 + 200 + font13 + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-search.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-search.xml new file mode 100644 index 000000000..95db79fb5 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-search.xml @@ -0,0 +1,8839 @@ + + SetProperty(dropdown,1) + + 1 + 0 + 0 + + + + + + 0 + 0 + 564 + 1080 + script.plex/white-square.png + + + 0 + 135 + 564 + 810 + String.IsEmpty(Window.Property(hide.kbd)) + script.plex/white-square.png + + + 0 + 135 + 564 + 248 + !String.IsEmpty(Window.Property(hide.kbd)) + script.plex/white-square.png + + + 0 + 945 + 564 + 135 + String.IsEmpty(Window.Property(hide.kbd)) + script.plex/white-square.png + + + 0 + 383 + 564 + 697 + !String.IsEmpty(Window.Property(hide.kbd)) + script.plex/white-square.png + + + + + Focus + UnFocus + 60 + 47.5 + 40 + 40 + 900 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + Close + + + + false + 160 + 35 + 500 + 65 + font12 + left + center + FFFFFFFF + + + + + + + 60 + 186 + 459 + 60 + + + 0 + 0 + + 0 + 0 + 151 + 60 + script.plex/white-square.png + + + String.IsEqual(Window.Property(search.section),all) + + 0 + 0 + 151 + 60 + script.plex/white-square.png + + + false + 0 + 0 + 151 + 60 + font12 + center + center + FF000000 + + + + + false + !String.IsEqual(Window.Property(search.section),all) + 0 + 0 + 151 + 60 + font12 + center + center + FFFFFFFF + + + + + 154 + 0 + + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + String.IsEqual(Window.Property(search.section),movie) + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + + 231 + 0 + + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + String.IsEqual(Window.Property(search.section),show) + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + + 308 + 0 + + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + String.IsEqual(Window.Property(search.section),artist) + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + + 384 + 0 + + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + String.IsEqual(Window.Property(search.section),photo) + 0 + 0 + 74 + 60 + script.plex/white-square.png + + + + + 901 + + 0 + 0 + + Focus + UnFocus + + -40 + -40 + 231 + 140 + 902 + 999 + 650 + font12 + center + center + FF000000 + 00000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 140 + 901 + 903 + 999 + 650 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 231 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 140 + 902 + 904 + 999 + 650 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 140 + 903 + 905 + 999 + 650 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 385 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 140 + 904 + 3000 + 999 + 650 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + 154 + 0 + + Conditional + 0 + 0 + 74 + 60 + + 26 + 20 + 22 + 20 + script.plex/home/type/movie.png + + + + + 231 + 0 + + Conditional + 0 + 0 + 74 + 60 + + 26 + 20 + 24 + 20 + script.plex/home/type/show.png + + + + + 308 + 0 + + Conditional + 0 + 0 + 74 + 60 + + 26 + 20 + 18 + 20 + script.plex/home/type/artist.png + + + + + 385 + 0 + + Conditional + 0 + 0 + 74 + 60 + + 26 + 20 + 22 + 20 + script.plex/home/type/photo.png + + + + + + + + 60 + 276 + 459 + 60 + + 0 + 0 + 459 + 60 + script.plex/white-square.png + + + 0 + 0 + 459 + 60 + left + center + 900 + 1001 + 3000 + 00000000 + + + font13 + 30 + script.plex/home/selected.png + - + no + + + false + 30 + 0 + 399 + 60 + left + center + FFFFFFFF + font13 + + + + + + + 60 + 360 + 459 + 439 + String.IsEmpty(Window.Property(hide.kbd)) + + + + 0 + 0 + + 0 + 0 + 74 + 74 + script.plex/white-square.png + + + 77 + 0 + 74 + 74 + script.plex/white-square.png + + + 154 + 0 + 74 + 74 + script.plex/white-square.png + + + 231 + 0 + 74 + 74 + script.plex/white-square.png + + + 308 + 0 + 74 + 74 + script.plex/white-square.png + + + 385 + 0 + 74 + 74 + script.plex/white-square.png + + + + + 0 + 77 + + 0 + 0 + 74 + 74 + script.plex/white-square.png + + + 77 + 0 + 74 + 74 + script.plex/white-square.png + + + 154 + 0 + 74 + 74 + script.plex/white-square.png + + + 231 + 0 + 74 + 74 + script.plex/white-square.png + + + 308 + 0 + 74 + 74 + script.plex/white-square.png + + + 385 + 0 + 74 + 74 + script.plex/white-square.png + + + + + 0 + 154 + + 0 + 0 + 74 + 74 + script.plex/white-square.png + + + 77 + 0 + 74 + 74 + script.plex/white-square.png + + + 154 + 0 + 74 + 74 + script.plex/white-square.png + + + 231 + 0 + 74 + 74 + script.plex/white-square.png + + + 308 + 0 + 74 + 74 + script.plex/white-square.png + + + 385 + 0 + 74 + 74 + script.plex/white-square.png + + + + + 0 + 231 + + 0 + 0 + 74 + 74 + script.plex/white-square.png + + + 77 + 0 + 74 + 74 + script.plex/white-square.png + + + 154 + 0 + 74 + 74 + script.plex/white-square.png + + + 231 + 0 + 74 + 74 + script.plex/white-square.png + + + 308 + 0 + 74 + 74 + script.plex/white-square.png + + + 385 + 0 + 74 + 74 + script.plex/white-square.png + + + + + 0 + 308 + + 0 + 0 + 74 + 74 + script.plex/white-square.png + + + 77 + 0 + 74 + 74 + script.plex/white-square.png + + + 154 + 0 + 74 + 74 + script.plex/white-square.png + + + 231 + 0 + 74 + 74 + script.plex/white-square.png + + + 308 + 0 + 74 + 74 + script.plex/white-square.png + + + 385 + 0 + 74 + 74 + script.plex/white-square.png + + + + + 0 + 385 + + 0 + 0 + 74 + 74 + script.plex/white-square.png + + + 77 + 0 + 74 + 74 + script.plex/white-square.png + + + 154 + 0 + 74 + 74 + script.plex/white-square.png + + + 231 + 0 + 74 + 74 + script.plex/white-square.png + + + 308 + 0 + 74 + 74 + script.plex/white-square.png + + + 385 + 0 + 74 + 74 + script.plex/white-square.png + + + + + + 0 + 0 + + 0 + 0 + + true + Focus + UnFocus + + -40 + -40 + 154 + 154 + 650 + 1007 + 1002 + 1006 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 77 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 650 + 1008 + 1003 + 1001 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 650 + 1009 + 1004 + 1002 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 231 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 650 + 1010 + 1005 + 1003 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 650 + 1011 + 1006 + 1004 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 385 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 650 + 1012 + 3000 + 1005 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + 0 + 77 + + 0 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1001 + 1013 + 1008 + 1012 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 77 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1002 + 1014 + 1009 + 1007 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1003 + 1015 + 1010 + 1008 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 231 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1004 + 1016 + 1011 + 1009 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1005 + 1017 + 1012 + 1010 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 385 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1006 + 1018 + 3000 + 1011 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + 0 + 154 + + 0 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1007 + 1019 + 1014 + 1018 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 77 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1008 + 1020 + 1015 + 1013 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1009 + 1021 + 1016 + 1014 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 231 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1010 + 1022 + 1017 + 1015 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1011 + 1023 + 1018 + 1016 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 385 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1012 + 1024 + 3000 + 1017 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + 0 + 231 + + 0 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1013 + 1025 + 1020 + 1024 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 77 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1014 + 1026 + 1021 + 1019 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1015 + 1027 + 1022 + 1020 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 231 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1016 + 1028 + 1023 + 1021 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1017 + 1029 + 1024 + 1022 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 385 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1018 + 1030 + 3000 + 1023 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + 0 + 308 + + 0 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1019 + 1031 + 1026 + 1030 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 77 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1020 + 1032 + 1027 + 1025 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1021 + 1033 + 1028 + 1026 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 231 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1022 + 1034 + 1029 + 1027 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1023 + 1035 + 1030 + 1028 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 385 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1024 + 1036 + 3000 + 1029 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + 0 + 385 + + 0 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1025 + 951 + 1032 + 1036 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 77 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1026 + 951 + 1033 + 1031 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1027 + 952 + 1034 + 1032 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 231 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1028 + 952 + 1035 + 1033 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1029 + 953 + 1036 + 1034 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 385 + 0 + + Focus + UnFocus + + -40 + -40 + 154 + 154 + 1030 + 953 + 3000 + 1035 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + + + 60 + 840 + 459 + 60 + String.IsEmpty(Window.Property(hide.kbd)) + + 0 + 0 + 151 + 60 + script.plex/white-square.png + + + 154 + 0 + 151 + 60 + script.plex/white-square.png + + + 308 + 0 + 151 + 60 + script.plex/white-square.png + + + + 0 + 0 + + Focus + UnFocus + + -40 + -40 + 231 + 140 + 953 + 952 + 1031 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 154 + 0 + + Focus + UnFocus + + -40 + -40 + 231 + 140 + 951 + 953 + 1033 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + 308 + 0 + + Focus + UnFocus + + -40 + -40 + 231 + 140 + 952 + 3000 + 1036 + font12 + center + center + FF000000 + script.plex/white-square-rounded-with-shadow.png + - + + + + + + + Visible + !String.IsEmpty(Window.Property(searching)) + 0 + 993 + + false + 420 + 0 + 434 + 39 + font12 + right + center + FFFFFFFF + + + + 480 + 0 + + !String.IsEmpty(Window.Property(searching)) + Visible + 0 + 0 + 39 + 39 + script.plex/indicators/spinner.png + + + + + !String.IsEmpty(Window.Property(no.results)) + String.IsEmpty(Window.Property(searching)) + 0 + 993 + + false + 0 + 0 + 564 + 39 + font12 + center + center + FFFFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2000 + 564 + 20 + 2130 + 5540 + 20 + vertical + true + 200 + + + 2100 + Integer.IsGreater(Container(2100).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2101 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2100) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2100) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2100 + 2102 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2101) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2101) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2102).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2101 + 2103 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2102) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2102) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2103).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2102 + 2104 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2103) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2103) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2104).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2103 + 2105 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2104) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2104) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2105).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2104 + 2106 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2105) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2105) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2106).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2105 + 2107 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2106) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2106) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2107).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2106 + 2108 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2107) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2107) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2108).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2107 + 2109 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2108) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2108) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2109).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2108 + 2110 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2109) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2109) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2110).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2109 + 2111 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2110) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2110) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2111).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2110 + 2112 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2111) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2111) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2112).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2111 + 2113 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2112) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2112) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2113).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2112 + 2114 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2113) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2113) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2114).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2113 + 2115 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2114) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2114) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2115).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2114 + 2116 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2115) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2115) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2116).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2115 + 2117 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2116) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2116) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2117).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2116 + 2118 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2117) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2117) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2118).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2117 + 2119 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2118) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2118) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2119).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2118 + 2120 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2119) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2119) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2120).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2119 + 2121 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2120) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2120) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2121).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2120 + 2122 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2121) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2121) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2122).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2121 + 2123 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2122) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2122) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2123).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2122 + 2124 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2123) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2123) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2124).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2123 + 2125 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2124) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2124) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2125).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2124 + 2126 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2125) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2125) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2126).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2125 + 2127 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2126) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2126) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2127).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2126 + 2128 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2127) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2127) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2128).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2127 + 2129 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2128) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2128) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2129).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2128 + 2130 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2129) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2129) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2130).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2129 + 2131 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2130) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2130) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2131).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2130 + 2132 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2131) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2131) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2132).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2131 + 2133 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2132) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2132) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2133).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2132 + 2134 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2133) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2133) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2134).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2133 + 2135 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2134) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2134) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2135).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2134 + 2136 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2135) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2135) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2136).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2135 + 2137 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2136) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2136) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2137).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2136 + 2138 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2137) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2137) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2138).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2137 + 2139 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2138) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2138) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2139).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2138 + 2140 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2139) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2139) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2140).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2139 + 2141 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2140) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2140) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2141).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2140 + 2142 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2141) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2141) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2142).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2141 + 2143 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2142) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2142) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2143).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2142 + 2144 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2143) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2143) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + Integer.IsGreater(Container(2144).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2143 + 2145 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2144) + -40 + -40 + 270 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 180 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 180 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 180 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2144) + 0 + 0 + 190 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2145).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2144 + 2146 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2145) + -40 + -40 + 360 + 360 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 270 + 270 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 270 + 270 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 280 + 270 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2145) + 0 + 0 + 280 + 280 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2146).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 320 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2145 + 2147 + 200 + horizontal + 4 + + + + 55 + 64 + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 64 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2146) + -40 + -40 + 390 + 259 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 300 + 169 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 300 + 169 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 179 + 300 + 35 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(2146) + 0 + 0 + 310 + 179 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(2147).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 1356 + 420 + + 60 + 0 + 1236 + 2 + script.plex/white-square.png + FF282828 + + + false + 60 + 0 + 1000 + 87 + font12 + left + center + FFFFFFFF + + + + 0 + 30 + 1356 + 440 + 899 + 2146 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(2147) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 254 + 244 + 35 + font10 + center + FFFFFFFF + + + + 0 + 282 + 244 + 35 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(2147) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + + \ No newline at end of file diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml new file mode 100644 index 000000000..c86327de0 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml @@ -0,0 +1,1361 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + + + + + + + + + + + + + + 0 + 155 + 101 + + + VisibleChange + !String.IsEmpty(Window.Property(initialized)) + 302 + 440 + 445 + 1000 + 145 + 200 + 400 + -20 + horizontal + 200 + true + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/info-focus.png + script.plex/buttons/info.png + + + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + + 0 + 0 + 1920 + 600 + + 60 + 0 + 347 + 518 + script.plex/thumb_fallbacks/movie.png + WindowOpen + scale + + + 60 + 0 + 347 + 518 + $INFO[Window.Property(thumb)] + scale + + + + 466 + 0 + 1360 + 60 + font13 + left + FFFFFFFF + + + + 466 + 70 + 1360 + 30 + left + 0 + horizontal + true + + auto + 30 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Window.Property(rating.stars)) + auto + 30 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Window.Property(rating.stars)) + 4 + 134 + 22 + script.plex/stars/$INFO[Window.Property(rating.stars)].png + + + + + !String.IsEmpty(Window.Property(rating)) + 1660 + 70 + 200 + 32 + right + 15 + horizontal + true + + 2 + 63 + 30 + $INFO[Window.Property(rating.image)] + keep + + + auto + 30 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Window.Property(rating2)) + 2 + 63 + 30 + $INFO[Window.Property(rating2.image)] + keep + + + !String.IsEmpty(Window.Property(rating2)) + auto + 30 + font12 + left + FFFFFFFF + + + + + + + !String.IsEmpty(Window.Property(directors)) + 466 + 130 + 1360 + 30 + font12 + left + 99FFFFFF + + + + !String.IsEmpty(Window.Property(writers)) + 466 + 165 + 1360 + 30 + font12 + left + 99FFFFFF + + + + + 466 + 234 + 1360 + 179 + font12 + left + FFFFFFFF + + + + WindowOpen + -1 + 557 + 1 + 8 + script.plex/white-square.png + FFCC7B19 + + + !Control.IsVisible(500) + 0 + 565 + 1920 + 2 + script.plex/white-square.png + A0000000 + + + + + 0 + 565 + 1920 + 1600 + + 300 + 0 + + + Integer.IsGreater(Container(400).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 360 + 1920 + + 0 + 0 + 1920 + 360 + script.plex/white-square.png + 40000000 + + + 0 + 14 + 1920 + 430 + 300 + 401 + 200 + horizontal + 4 + + + + 55 + 29 + + 5 + 5 + + 0 + 0 + 158 + 236 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 158 + 236 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 113 + 0 + 45 + 40 + script.plex/white-square.png + FF000000 + + + 114 + 0 + 44 + 39 + script.plex/white-square.png + FFCC7B19 + + + 114 + 0 + 44 + 39 + font10 + center + center + FF000000 + + + + + false + 0 + 240 + 158 + 54 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 29 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(400) + -40 + -40 + 248 + 326 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 158 + 236 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 158 + 236 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 113 + 0 + 45 + 40 + script.plex/white-square.png + FF000000 + + + 114 + 0 + 44 + 39 + script.plex/white-square.png + FFCC7B19 + + + 114 + 0 + 44 + 39 + font10 + center + center + FF000000 + + + + + Control.HasFocus(400) + 0 + 240 + 158 + 54 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(400) + 0 + 0 + 168 + 246 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(401).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 360 + 1920 + + 60 + 0 + 800 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 18 + 1920 + 430 + 400 + 402 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + false + 0 + 175 + 299 + 60 + font12 + center + FFFFFFFF + + + + false + 0 + 210 + 299 + 60 + font12 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(401) + -40 + -40 + 389 + 258 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + + Control.HasFocus(401) + 0 + 175 + 299 + 60 + font12 + center + FFFFFFFF + + + + Control.HasFocus(401) + 0 + 210 + 299 + 60 + font12 + center + FFFFFFFF + + + + + + Control.HasFocus(401) + 0 + 0 + 309 + 178 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(402).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 402 + 1920 + 520 + + !String.IsEmpty(Window.Property(divider.402)) + 60 + 0 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 16 + 1920 + 520 + 401 + 403 + false + false + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(402) + -40 + -40 + 324 + 441 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + Control.HasFocus(402) + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(402) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + + + + + + + Integer.IsGreater(Container(403).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 403 + 1920 + 410 + + !String.IsEmpty(Window.Property(divider.403)) + 60 + 20 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 20 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 36 + 1920 + 410 + 402 + 403 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + 244 + 90 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(403) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + 244 + 90 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(403) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + + + Conditional + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + !String.IsEmpty(Window.Property(on.extras)) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml new file mode 100644 index 000000000..d85f26fc4 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml @@ -0,0 +1,1106 @@ + + + + 1 + 0 + 0 + + 100 + 800 + + + [!String.IsEmpty(Window.Property(show.OSD)) | Window.IsVisible(seekbar) | !String.IsEmpty(Window.Property(button.seek))] + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsActive(playerprocessinfo) + !Window.IsActive(selectdialog) + Hidden + + String.IsEmpty(Window.Property(settings.visible)) + [Window.IsVisible(seekbar) | Window.IsVisible(videoosd) | Player.ShowInfo] + Hidden + 0 + 0 + + 0 + 0 + 1920 + 1080 + script.plex/player-fade.png + FF080808 + + + + + 0 + 0 + + 0 + 0 + 1920 + 140 + script.plex/white-square.png + A0000000 + + + 0 + 940 + 1920 + 140 + script.plex/white-square.png + A0000000 + + + + + 0 + 40 + + !String.IsEmpty(Window.Property(is.show)) + 60 + 0 + 1500 + 60 + font13 + left + center + FFFFFFFF + + + + String.IsEmpty(Window.Property(is.show)) + 60 + 0 + 1500 + 60 + font13 + left + center + FFFFFFFF + + + + 1860 + 0 + 300 + 60 + font12 + right + center + FFFFFFFF + + + + + + 0 + 965 + + !String.IsEmpty(Window.Property(direct.play)) + 60 + 0 + 1000 + 60 + font13 + left + center + FFFFFFFF + + + + String.IsEmpty(Window.Property(direct.play)) + 60 + 0 + 1000 + 60 + font13 + left + center + FFFFFFFF + + + + Player.IsTempo + 60 + 40 + 1000 + 60 + font13 + left + center + A0FFFFFF + + + + !String.IsEmpty(Window.Property(direct.play)) + 1860 + 0 + 800 + 60 + font13 + right + center + FFFFFFFF + + + + String.IsEmpty(Window.Property(direct.play)) + 1860 + 0 + 800 + 60 + font13 + right + center + FFFFFFFF + + + + !String.IsEmpty(Window.Property(media.show_ends)) + !String.IsEmpty(Window.Property(direct.play)) + 1860 + 40 + 800 + 60 + font13 + right + center + A0FFFFFF + + + + !String.IsEmpty(Window.Property(media.show_ends)) + String.IsEmpty(Window.Property(direct.play)) + 1860 + 40 + 800 + 60 + font13 + right + center + A0FFFFFF + + + + Player.Paused + String.IsEmpty(Window.Property(show.OSD)) + Visible + 0 + 20 + 1920 + 60 + font13 + center + center + FFCC7B19 + + + + + + 0 + 940 + + 0 + 0 + 1920 + 10 + script.plex/white-square.png + A0000000 + + + !String.IsEmpty(Window.Property(show.buffer)) + 0 + 2 + 1 + 6 + script.plex/white-square.png + EE4E4842 + + + 0 + 2 + 1 + 6 + script.plex/white-square.png + FFAC5B00 + + + Control.HasFocus(100) | !String.IsEmpty(Window.Property(button.seek)) + 0 + 2 + 1 + 6 + script.plex/white-square.png + FFE5A00D + + + + + String.IsEmpty(Window.Property(show.OSD)) + 0 + 0 + 1920 + 1080 + - + - + + SetProperty(show.OSD,1) + + + + + 0 + 350 + !String.IsEmpty(Window.Property(show.PPI)) + String.IsEmpty(Window.Property(settings.visible)) + String.IsEmpty(Window.Property(playlist.visible)) + Visible + Hidden + + 10 + -220 + 10 + 420 + buttons/dialogbutton-nofo.png + + + 52 + -184 + 1786 + 350 + horizontal + 10 + + 0 + 0 + 793 + + 793 + 50 + bottom + + font14 + black + Player.HasVideo + + + 793 + 50 + bottom + + font14 + black + Player.HasVideo + + + 793 + 50 + bottom + + font14 + black + Player.HasVideo + + + 793 + 50 + bottom + + font14 + black + Player.HasVideo + + + 793 + 50 + bottom + + + font14 + black + + + 793 + 50 + bottom + + font14 + black + + + + 0 + 0 + 993 + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Status)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Mode)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Container)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Video)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Audio)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Subtitles)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.User)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + String.IsEmpty(Window.Property(ppi.Buffered)) + + + 893 + 50 + bottom + + font14 + black + Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Buffered)) + + + + + 52 + 120 + 1786 + 50 + bottom + + font14 + black + + + + !String.IsEmpty(Window.Property(show.OSD)) + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsActive(playerprocessinfo) + !Window.IsActive(selectdialog) + Hidden + + !String.IsEmpty(Window.Property(has.bif)) + [Control.HasFocus(100) | Control.HasFocus(501) | !String.IsEmpty(Window.Property(button.seek))] + Visible + 0 + 752 + + 0 + 0 + 324 + 184 + script.plex/white-square.png + FF000000 + + + 2 + 2 + 320 + 180 + 10 + $INFO[Window.Property(bif.image)] + + + + + 406 + + 360 + 964 + 1200 + + 124 + center + 100 + -40 + horizontal + 200 + true + + !String.IsEmpty(Window.Property(nav.repeat)) + Conditional + Conditional + 125 + 101 + + + 0 + 0 + 125 + 101 + 100 + 402 + 412 + font12 + - + - + + + + !Control.HasFocus(401) + + !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat.png + + + Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat.png + + + Playlist.IsRepeatOne | !String.IsEmpty(Window.Property(pq.repeat.one)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-one.png + + + + Control.HasFocus(401) + + !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-focus.png + + + Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-focus.png + + + Playlist.IsRepeatOne | !String.IsEmpty(Window.Property(pq.repeat.one)) + 0 + 0 + 125 + 101 + script.plex/buttons/repeat-one-focus.png + + + + + + !String.IsEmpty(Window.Property(has.playlist)) + !String.IsEmpty(Window.Property(nav.shuffle)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + !String.IsEmpty(Window.Property(pq.shuffled)) + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + false + String.IsEmpty(Window.Property(has.playlist)) + !String.IsEmpty(Window.Property(nav.shuffle)) + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/settings-focus.png + script.plex/buttons/settings.png + + + + + + !String.IsEmpty(Window.Property(pq.hasprev)) + !String.IsEmpty(Window.Property(nav.prevnext)) + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + false + String.IsEmpty(Window.Property(pq.hasprev)) + !String.IsEmpty(Window.Property(nav.prevnext)) + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + !String.IsEmpty(Window.Property(nav.ffwdrwd)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/skip-forward-focus.png + script.plex/buttons/skip-forward.png + + + + + Conditional + Conditional + 125 + 101 + + + 0 + 0 + 125 + 101 + 100 + 407 + 405 + font12 + - + - + + PlayerControl(Play) + + + !Control.HasFocus(406) + + !Player.Paused + !Player.Forwarding + !Player.Rewinding + 0 + 0 + 125 + 101 + script.plex/buttons/pause.png + + + Player.Paused | Player.Forwarding | Player.Rewinding + 0 + 0 + 125 + 101 + script.plex/buttons/play.png + + + + Control.HasFocus(406) + + !Player.Paused + !Player.Forwarding + !Player.Rewinding + 0 + 0 + 125 + 101 + script.plex/buttons/pause-focus.png + + + Player.Paused | Player.Forwarding | Player.Rewinding + 0 + 0 + 125 + 101 + script.plex/buttons/play-focus.png + + + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/stop-focus.png + script.plex/buttons/stop.png + + + + !String.IsEmpty(Window.Property(nav.ffwdrwd)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/skip-forward-focus.png + script.plex/buttons/skip-forward.png + + + + !String.IsEmpty(Window.Property(pq.hasnext)) + !String.IsEmpty(Window.Property(nav.prevnext)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + false + String.IsEmpty(Window.Property(pq.hasnext)) + !String.IsEmpty(Window.Property(nav.prevnext)) + 0 + 0 + 125 + 101 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + + + + + + [!String.IsEmpty(Window.Property(pq.hasnext)) | !String.IsEmpty(Window.Property(pq.hasprev))] + !String.IsEmpty(Window.Property(nav.playlist)) + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/pqueue-focus.png + script.plex/buttons/pqueue.png + + + + false + String.IsEmpty(Window.Property(pq.hasnext)) + String.IsEmpty(Window.Property(pq.hasprev)) + !String.IsEmpty(Window.Property(nav.playlist)) + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/pqueue-focus.png + script.plex/buttons/pqueue.png + + + + !String.IsEmpty(Window.Property(nav.quick_subtitles)) + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/subtitle-focus.png + script.plex/buttons/subtitle.png + + + + + + 0 + 940 + + + 0 + 0 + 1920 + 10 + 501 + 400 + - + - + + + + + Conditional + + + Conditional + + + String.IsEmpty(Window.Property(mouse.mode)) + String.IsEmpty(Window.Property(hide.bigseek)) + [Control.HasFocus(501) | Control.HasFocus(100)] + [!String.IsEmpty(Window.Property(show.chapters)) | String.IsEmpty(Window.Property(has.chapters))] + -8 + 917 + + -200 + 5 + 2320 + 6 + script.plex/white-square.png + A0000000 + String.IsEmpty(Window.Property(has.chapters)) + + + + 0 + -175 + 1928 + 200 + script.plex/white-square.png + A0000000 + !String.IsEmpty(Window.Property(has.chapters)) + + + 40 + -162 + auto + 20 + font10 + left + center + CC606060 + + !String.IsEmpty(Window.Property(has.chapters)) + !Control.HasFocus(501) + + + 40 + -162 + auto + 20 + font10 + left + center + FFFFFFFF + + !String.IsEmpty(Window.Property(has.chapters)) + Control.HasFocus(501) + + + + + 0 + 0 + 1928 + 16 + 100 + SetProperty(hide.bigseek,) + 200 + horizontal + 4 + + + + 0 + 0 + 16 + 16 + script.plex/indicators/seek-selection-marker.png + FF606060 + + + + + + + !Control.HasFocus(501) + 0 + 0 + 16 + 16 + script.plex/indicators/seek-selection-marker.png + FF606060 + + + Control.HasFocus(501) + 0 + 0 + 16 + 16 + script.plex/indicators/seek-selection-marker.png + FFE5A00D + + + + + + + + 40 + 0 + 178 + 100 + script.plex/thumb_fallbacks/movie16x9.png + scale + CC606060 + !Control.HasFocus(501) + + + 40 + 0 + 178 + 100 + $INFO[ListItem.Thumb] + scale + DDAAAAAA + !Control.HasFocus(501) + + + 40 + 0 + 178 + 100 + script.plex/thumb_fallbacks/movie16x9.png + scale + FFAAAAAA + Control.HasFocus(501) + + + 40 + 0 + 178 + 100 + $INFO[ListItem.Thumb] + scale + FFAAAAAA + Control.HasFocus(501) + + + 40 + 120 + auto + 10 + font10 + center + center + CC606060 + + !Control.HasFocus(501) + + + 40 + 120 + auto + 10 + font10 + center + center + FFAAAAAA + + Control.HasFocus(501) + + + + + + + + + 40 + 0 + 178 + 100 + script.plex/thumb_fallbacks/movie16x9.png + scale + CC909090 + !Control.HasFocus(501) + + + 40 + 0 + 178 + 100 + $INFO[ListItem.Thumb] + scale + FFAAAAAA + !Control.HasFocus(501) + + + 40 + 0 + 178 + 100 + script.plex/thumb_fallbacks/movie16x9.png + scale + + Control.HasFocus(501) + + + 40 + 0 + 178 + 100 + $INFO[ListItem.Thumb] + scale + + Control.HasFocus(501) + + + 40 + 120 + auto + 10 + font10 + center + center + FFAAAAAA + + !Control.HasFocus(501) + + + 40 + 120 + auto + 10 + font10 + center + center + + + Control.HasFocus(501) + + + + + + + + Control.HasFocus(100) | Control.HasFocus(501) | !String.IsEmpty(Window.Property(button.seek)) + 0 + 896 + + -50 + 0 + + Visible + 0 + 0 + 101 + 39 + script.plex/indicators/player-selection-time_box.png + D0000000 + + + 0 + 0 + 101 + 40 + font10 + center + center + FFFFFFFF + + + + + Visible + -6 + 39 + 15 + 7 + script.plex/indicators/player-selection-time_arrow.png + D0000000 + + + + + + 30 + 797 + 1670 + 143 + right + horizontal + + [!String.IsEmpty(Window.Property(show.markerSkip)) + String.IsEmpty(Window.Property(show.markerSkip_OSDOnly))] | [!String.IsEmpty(Window.Property(show.markerSkip_OSDOnly)) + !String.IsEmpty(Window.Property(show.OSD))] + Focus + UnFocus + + + + auto + 143 + center + 0 + 0 + script.plex/buttons/blank-focus.png + script.plex/buttons/blank.png + 70 + FF000000 + FF000000 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-settings.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-settings.xml new file mode 100644 index 000000000..3e2cdece4 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-settings.xml @@ -0,0 +1,684 @@ + + + 300 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + + Conditional + 0 + 0 + + + Conditional + + 843 + 135 + 1077 + 945 + script.plex/white-square.png + 32111111 + + + 0 + 810 + 1920 + 270 + script.plex/white-square.png + 32000000 + + + + 843 + 810 + 1077 + 270 + script.plex/white-square.png + 32111111 + + + Conditional + 1920 + 135 + 879 + 945 + script.plex/white-square.png + 66111111 + + + + + Control.HasFocus(100) | Control.HasFocus(125) + 903 + 870 + 1017 + 170 + font10 + FFFFFFFF + left + 200 + + + + + + 248 + 168 + 75 + + 0 + 0 + 590 + 666 + 201 + 100 + 200 + vertical + + + + 88 + 40 + + 0 + 0 + 414 + 75 + font12 + left + center + FFFFFFFF + + + + 0 + 75 + 414 + 2 + script.plex/white-square.png + 661F1F1F + + + + + + ControlGroup(50).HasFocus(0) + + 0 + 0 + 590 + 155 + script.plex/drop-shadow.png + + + Control.HasFocus(75) + 40 + 40 + 510 + 75 + script.plex/white-square-rounded.png + + + !Control.HasFocus(75) + 40 + 40 + 510 + 75 + script.plex/white-square-rounded.png + + + !Control.HasFocus(75) + 512 + 66 + 16 + 23 + script.plex/settings/expanded.png + FF000000 + + + 88 + 40 + 414 + 75 + font12 + left + center + FF000000 + + + + + !ControlGroup(50).HasFocus(0) + 88 + 40 + 414 + 75 + font12 + left + center + FFFFFFFF + + + + + + + Integer.IsGreater(Container(100).NumItems,0) + String.IsEmpty(Window.Property(section.about)) + 604 + 0 + 776 + 666 + 75 + 150 + 200 + vertical + 101 + + + + 88 + 40 + + 0 + 0 + 500 + 75 + font12 + left + center + FFFFFFFF + + + + 200 + 0 + 400 + true + 25 + 75 + font12 + right + center + FFFFFFFF + + + + 0 + 75 + 600 + 2 + script.plex/white-square.png + 661F1F1F + + + + 643 + 55 + + !String.IsEmpty(ListItem.Property(checkbox)) + 0 + 0 + 45 + 45 + script.plex/settings/checkbox.png + + + !String.IsEmpty(ListItem.Property(checkbox.checked)) + 0 + 0 + 45 + 45 + script.plex/settings/checkmark.png + + + + + + Control.HasFocus(100) | Control.HasFocus(125) + + 0 + 0 + 776 + 155 + script.plex/drop-shadow.png + + + !Control.HasFocus(125) + 40 + 40 + 696 + 75 + script.plex/white-square-rounded.png + + + Control.HasFocus(125) + 40 + 40 + 696 + 75 + script.plex/white-square-rounded.png + + + Control.HasFocus(125) + 698 + 66 + 16 + 23 + script.plex/settings/expanded.png + FF000000 + + + 88 + 40 + + 0 + 0 + 500 + 75 + font12 + left + center + FF000000 + + + + 200 + 0 + 400 + 75 + font12 + right + center + FF000000 + + + + + + !Control.HasFocus(100) + !Control.HasFocus(125) + 88 + 40 + + 0 + 0 + 400 + 75 + font12 + left + center + FFFFFFFF + + + + 200 + 0 + 400 + 75 + font12 + right + center + FFFFFFFF + + + + 0 + 75 + 600 + 2 + script.plex/white-square.png + 661F1F1F + + + + 643 + 55 + + !String.IsEmpty(ListItem.Property(checkbox)) + 0 + 0 + 45 + 45 + script.plex/settings/checkbox.png + + + !String.IsEmpty(ListItem.Property(checkbox.checked)) + 0 + 0 + 45 + 45 + script.plex/settings/checkmark.png + + + + + + + Control.IsVisible(100) + !Control.IsVisible(125) + 1388 + 40 + 9 + 525 + true + script.plex/white-square-rounded-4r.png + script.plex/white-square-rounded-4r.png + script.plex/white-square-rounded-4r.png + - + - + false + vertical + false + 151 + + + + Integer.IsGreater(Container(100).NumItems,0) + String.IsEmpty(Window.Property(section.about)) + 10 + 10 + 125 + 100 + font12 + FF000000 + - + - + + SetFocus(100) + + + + Control.HasFocus(125) + Integer.IsGreater(Container(100).NumItems,0) + Integer.IsGreater(Container(100).NumItems,0) + String.IsEmpty(Window.Property(section.about)) + + 1383 + 0 + 845 + 566 + 100 + 200 + vertical + 126 + + + + 88 + 40 + + 0 + 0 + 594 + 75 + font12 + left + center + FFFFFFFF + + + + 0 + 75 + 594 + 2 + script.plex/white-square.png + 661F1F1F + + + + 637 + 55 + + !String.IsEmpty(ListItem.Property(checkbox.checked)) + 0 + 0 + 45 + 45 + script.plex/settings/checkmark.png + + + + + + 0 + 0 + 610 + 155 + script.plex/drop-shadow.png + + + 40 + 40 + 690 + 75 + script.plex/white-square-rounded.png + + + 88 + 40 + + 0 + 0 + 594 + 75 + font12 + left + center + FF000000 + + + + + 637 + 55 + + !String.IsEmpty(ListItem.Property(checkbox.checked)) + 0 + 0 + 45 + 45 + script.plex/settings/checkmark.png + + + + + + + Control.IsVisible(125) + 2161 + 40 + 9 + 525 + true + script.plex/white-square-rounded-4r.png + script.plex/white-square-rounded-4r.png + script.plex/white-square-rounded-4r.png + - + - + false + vertical + false + 151 + + + + + + + + 201 + 0 + 0 + 1920 + 135 + + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 201 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-settings_select_dialog.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-settings_select_dialog.xml new file mode 100644 index 000000000..f619fa164 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-settings_select_dialog.xml @@ -0,0 +1,94 @@ + + + + 1 + 0 + 0 + + + + + 660 + 145 + + -40 + -40 + 680 + 870 + script.plex/drop-shadow.png + + + 0 + 0 + 600 + 80 + script.plex/white-square-top-rounded.png + F21F1F1F + + + 0 + 80 + 600 + 710 + script.plex/white-square-top-rounded.png + F2606060 + + + 0 + 0 + 600 + 80 + font12 + center + center + FFFFFFFF + + + + 0 + 80 + 600 + 700 + noop + noop + 200 + vertical + + + + 20 + 0 + 560 + 100 + font12 + left + center + FFFFFFFF + + + + + + 0 + 0 + 600 + 100 + script.plex/white-square.png + + + 20 + 0 + 560 + 100 + font12 + left + center + FF000000 + + + + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-signin_background.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-signin_background.xml new file mode 100644 index 000000000..4747d5473 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-signin_background.xml @@ -0,0 +1,21 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + 0 + 0 + 1920 + 1080 + script.plex/sign_in/back.jpg + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-signin_blank.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-signin_blank.xml new file mode 100644 index 000000000..91e3fae5e --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-signin_blank.xml @@ -0,0 +1,49 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + 0 + 0 + 1920 + 1080 + script.plex/sign_in/back.jpg + + + + 210 + 375 + 1502 + 530 + font13 + FFFFFFFF + left + + + + + 0 + 0 + 1920 + 1080 + font20 + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-slideshow.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-slideshow.xml new file mode 100644 index 000000000..869b7255b --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-slideshow.xml @@ -0,0 +1,63 @@ + + 6 + + + + 0 + 0 + 1920 + 1080 + + keep + 1000 + $INFO[Window.Property(thumb)] + + + 20 + 20 + 100 + 1880 + + String.IsEqual(Window.Property(align),0) + 0 + 50 + font45 + left + FFFFFFFF + FF000000 + + + + String.IsEqual(Window.Property(align),0) + 50 + 20 + font13 + left + FFFFFFFF + FF000000 + + + + String.IsEqual(Window.Property(align),1) + 0 + 50 + font45 + right + FFFFFFFF + FF000000 + + + + String.IsEqual(Window.Property(align),1) + 50 + 20 + font13 + right + FFFFFFFF + FF000000 + + + + + + \ No newline at end of file diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-squares.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-squares.xml new file mode 100644 index 000000000..eac7b3e1d --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-squares.xml @@ -0,0 +1,769 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + + Conditional + Conditional + 0 + 135 + 101 + + + VisibleChange + !Integer.IsGreater(Container(101).ListItem.Property(index),5) + String.IsEmpty(Window.Property(no.content)) + String.IsEmpty(Window.Property(no.content.filtered)) + !String.IsEmpty(Window.Property(initialized)) + 301 + 30 + -25 + 1000 + 145 + 200 + 101 + -20 + horizontal + 200 + true + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + + + + !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/shuffle-focus.png + script.plex/buttons/shuffle.png + + + + + String.IsEmpty(Window.Property(no.options)) | Player.HasAudio + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/more-focus.png + script.plex/buttons/more.png + + + + + String.IsEmpty(Window.Property(hide.filteroptions)) + Focus + UnFocus + 0 + 0 + 126 + 100 + font12 + script.plex/buttons/chapters-focus.png + script.plex/buttons/chapters.png + + + + + + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 101 + 0 + 0 + 1920 + 1080 + + + 0 + 0 + 1800 + 1280 + 300 + 151 + 200 + vertical + 2 + 152 + + + + 55 + 97 + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 244 + 244 + 40 + script.plex/white-square.png + 80000000 + + + false + 0 + 260 + 244 + 40 + font10 + center + center + FFFFFFFF + + + + + + + + + + 55 + 97 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(101) + + String.IsEmpty(ListItem.Property(is.folder)) + -40 + -40 + 334 + 334 + script.plex/drop-shadow.png + + + !String.IsEmpty(ListItem.Property(is.folder)) + -40 + -40 + 334 + 374 + script.plex/drop-shadow.png + + + + 5 + 5 + + 0 + 0 + 244 + 244 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 244 + 244 + 40 + script.plex/white-square.png + 80000000 + + + Control.HasFocus(101) + 0 + 260 + 244 + 40 + font10 + center + center + FFFFFFFF + + + + + Control.HasFocus(101) + + String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 254 + 254 + script.plex/home/selected.png + + + !String.IsEmpty(ListItem.Property(is.folder)) + 0 + 0 + 254 + 294 + script.plex/home/selected.png + + + + + + + + + + + String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 151 + 1780 + 150 + 20 + 920 + + 0 + 0 + 34 + 1050 + 100 + 152 + 200 + vertical + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + + + + + 0 + 0 + + 0 + 0 + + !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) + 0 + 0 + 34 + 32 + font10 + center + center + 99FFFFFF + + + + String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) + 0 + 0 + 34 + 32 + font10 + center + center + FFE5A00D + + + + + + Control.HasFocus(151) + 0 + 0 + + Control.HasFocus(151) + 0 + 0 + 34 + 34 + FFE5A00D + script.plex/white-outline-rounded.png + + + + + + + + + + 1860 + 150 + 12 + 910 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + Conditional + 201 + 0 + 0 + 1920 + 135 + !String.IsEmpty(Window.Property(initialized)) + + VisibleChange + ControlGroup(200).HasFocus(0) + Integer.IsGreater(Container(101).ListItem.Property(index),5) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 211 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + String.IsEmpty(Window.Property(hide.filteroptions)) + 340 + 35 + 1000 + 65 + right + 30 + horizontal + 204 + 210 + 50 + + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + !String.IsEqual(Window.Property(media),artist) + false + auto + 65 + font12 + FFFFFFFF + FFFFFFFF + FFFFFFFF + center + center + - + - + 20 + 0 + + + + String.IsEqual(Window.Property(media),artist) + auto + 65 + font12 + FFFFFFFF + FF000000 + FFFFFFFF + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + auto + 65 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square-rounded.png + - + 20 + 0 + + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(no.content)) + 0 + 465 + + false + 60 + 0 + 1800 + 35 + font13 + center + FFFFFFFF + + + + false + 60 + 60 + 1800 + 35 + font13 + center + FFCCCCCC + + + + + + !String.IsEmpty(Window.Property(no.content.filtered)) + 0 + 465 + + false + 60 + 0 + 1800 + 35 + font13 + center + FFFFFFFF + + + + false + 60 + 60 + 1800 + 35 + font13 + center + FFCCCCCC + + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-track_context.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-track_context.xml new file mode 100644 index 000000000..1597a7053 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-track_context.xml @@ -0,0 +1,67 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 236 + 131 + 638 + 638 + $INFO[Window.Property(track.thumb)] + + + + 236 + 131 + 1502 + 530 + font13 + FFFFFFFF + left + + + + + VisibleChange + 301 + 965 + 131 + 750 + 638 + 30 + vertical + 200 + + 30 + 30 + 1920 + 1080 + font20 + FFFFFFFF + FFFFFFFF + center + center + - + - + 0 + 0 + + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-user_select.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-user_select.xml new file mode 100644 index 000000000..143893c08 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-user_select.xml @@ -0,0 +1,901 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour_opaque)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + + + Conditional + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 441 + 780 + + + 0 + 0 + 225 + 225 + $INFO[Player.Art(thumb)] + + + + 255 + 0 + + 0 + 0 + 783 + 40 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 0 + 40 + 783 + 40 + font10 + left + center + FFFFFFFF + MusicPlayer.Album + + + 0 + 80 + 783 + 40 + font10 + left + center + FFFFFFFF + + + + + + 406 + + 255 + 134 + 783 + 124 + center + 101 + -40 + horizontal + 200 + true + + + MusicPlayer.HasPrevious + Focus + UnFocus + + 30 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + PlayerControl(Previous) + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/pause-focus.png + script.plex/buttons/pause.png + Player.Paused | Player.Forwarding | Player.Rewinding + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + PlayerControl(Play) + + + + MusicPlayer.HasNext + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/next-focus.png + script.plex/buttons/next.png + PlayerControl(Next) + + + + Focus + UnFocus + + 0 + 0 + 125 + 101 + font12 + script.plex/buttons/stop-focus.png + script.plex/buttons/stop.png + PlayerControl(Stop) + + + + + + 255 + 177 + 783 + 40 + font10 + left + center + FFFFFFFF + MusicPlayer.Time + + + 1038 + 177 + 783 + 40 + font10 + right + center + FFFFFFFF + MusicPlayer.TimeRemaining + + + + + Progressbar + 255 + 222 + 783 + 3 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + + ControlGroup(400).HasFocus(0) + 770 + 275 + 380 + 695 + script.plex/drop-shadow.png + + + + 0 + 315 + 101 + + + -180 + -40 + 2100 + 455 + 200 + 500 + 600 + 400 + horizontal + 3 + + + + + + 0 + 40 + 300 + 300 + + 0 + 0 + $INFO[ListItem.Property(back.image)] + FFA0A0A0 + + + + 45 + 45 + 210 + 210 + script.plex/user_select/avatar-background.png + E0C0C0C0 + + + 54 + 54 + 192 + 192 + $INFO[ListItem.Thumb] + + + String.IsEmpty(ListItem.Thumb) + 54 + 54 + 192 + 192 + WeatherTemp + center + center + FFFFFFFF + + + + + !String.IsEmpty(ListItem.Property(protected)) + 15 + 231 + + 0 + 0 + 54 + 54 + script.plex/user_select/protected-back.png + A0000000 + + + 0 + 0 + 54 + 54 + script.plex/user_select/protected-icon.png + + + + + !String.IsEmpty(ListItem.Property(admin)) + 231 + 231 + + 0 + 0 + 54 + 54 + script.plex/user_select/admin-back.png + A0000000 + + + 0 + 0 + 54 + 54 + script.plex/user_select/admin-icon.png + + + + + + + + + + + 0 + 40 + + Control.HasFocus(101) + -40 + -40 + 380 + 455 + script.plex/drop-shadow.png + + + Control.HasFocus(101) | ControlGroup(400).HasFocus(0) + 0 + 0 + 300 + 300 + $INFO[ListItem.Property(back.image)] + FFA0A0A0 + + + !Control.HasFocus(101) + !ControlGroup(400).HasFocus(0) + 0 + 0 + 300 + 300 + $INFO[ListItem.Property(back.image)] + FFA0A0A0 + + + Control.HasFocus(101) + + 0 + 300 + 300 + 75 + script.plex/user_select/item-background-bottom.png + FF000000 + + + 0 + 300 + 300 + 75 + $INFO[ListItem.Property(back.image)] + stretch + 40FFFFFF + + + + + !Control.HasFocus(101) + !ControlGroup(400).HasFocus(0) + 45 + 45 + 210 + 210 + script.plex/user_select/avatar-background.png + E0C0C0C0 + + + ControlGroup(400).HasFocus(0) + 45 + 45 + 210 + 210 + script.plex/user_select/avatar-background.png + FFCC7B19 + + + Control.HasFocus(101) + 45 + 45 + 210 + 210 + script.plex/user_select/avatar-background.png + FFE5A00D + + + + 54 + 54 + 192 + 192 + $INFO[ListItem.Thumb] + + + String.IsEmpty(ListItem.Thumb) + 54 + 54 + 192 + 192 + WeatherTemp + center + center + FFFFFFFF + + + + + !String.IsEmpty(ListItem.Property(protected)) + 15 + 231 + + 0 + 0 + 54 + 54 + script.plex/user_select/protected-back.png + A0000000 + + + 0 + 0 + 54 + 54 + script.plex/user_select/protected-icon.png + + + + + !String.IsEmpty(ListItem.Property(admin)) + 231 + 231 + + 0 + 0 + 54 + 54 + script.plex/user_select/admin-back.png + A0000000 + + + 0 + 0 + 54 + 54 + script.plex/user_select/admin-icon.png + + + + + Control.HasFocus(101) + 10 + 300 + 280 + 75 + font13 + center + center + FFCC7B19 + + + + + + + + + 205 + !String.IsEmpty(Container(101).ListItem.Property(protected)) + ControlGroup(400).HasFocus(0) + !String.IsEmpty(Window.Property(initialized)) + 810 + 375 + + 0 + 0 + 300 + 239 + 400 + 400 + 400 + 400 + - + - + + + + 0 + -75 + 300 + 75 + script.plex/white-square.png + FF000000 + + + 0 + -75 + 300 + 75 + $INFO[Container(101).ListItem.Property(back.image)] + 40FFFFFF + + + String.IsEmpty(Container(101).ListItem.Property(editing.pin)) + 0 + -75 + 300 + 75 + font13 + center + center + FFCC7B19 + + + + !String.IsEmpty(Container(101).ListItem.Property(editing.pin)) + 0 + -75 + 300 + 75 + font13 + center + center + FFCC7B19 + + + + 0 + 0 + 300 + 239 + script.plex/white-square.png + FF000000 + + + 205 + + 205 + + 0 + 0 + 73 + 58 + 202 + 101 + 204 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 75 + 0 + 73 + 58 + 203 + 201 + 101 + 205 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 150 + 0 + 73 + 58 + 211 + 202 + 101 + 206 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 0 + 60 + 73 + 58 + 205 + 207 + 201 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 75 + 60 + 73 + 58 + 206 + 204 + 208 + 202 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 150 + 60 + 73 + 58 + 211 + 205 + 209 + 203 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 0 + 120 + 73 + 58 + 208 + 210 + 204 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 75 + 120 + 73 + 58 + 209 + 207 + 210 + 205 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + 150 + 120 + 73 + 58 + 211 + 208 + 210 + 206 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + + 0 + 180 + 223 + 59 + 211 + 200 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/white-square.png + script.plex/white-square.png + 0 + 0 + + + + + 225 + 0 + 75 + 239 + 300 + 101 + font12 + FFFFFFFF + FF000000 + center + center + script.plex/user_select/backspace.png + script.plex/user_select/backspace_nf.png + 0 + 0 + + + + + + + + 201 + 0 + 0 + 1920 + 135 + + 0 + 0 + 1920 + 135 + script.plex/white-square.png + 19000000 + + + + 60 + 34.5 + 1000 + 66 + left + 60 + horizontal + 101 + true + + 0 + 0 + 124 + 66 + + 0 + 0 + 124 + 66 + 101 + 101 + right + center + script.plex/white-square-rounded.png + - + + + + !String.IsEmpty(Window.Property(dropdown)) + 0 + 0 + 124 + 66 + script.plex/white-square-rounded.png + + + 27 + 13 + + !Control.HasFocus(500) + String.IsEmpty(Window.Property(dropdown)) + 0 + 0 + + 0 + 0 + 40 + 40 + script.plex/buttons/power.png + + + 55 + 13.5 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + + + + Control.HasFocus(500) | !String.IsEmpty(Window.Property(dropdown)) + 0 + 0 + + 0 + 0 + 40 + 40 + script.plex/buttons/power.png + + + 55 + 13.5 + 15 + 13 + script.plex/indicators/dropdown-triangle.png + + + + + + -27 + 0 + auto + 66 + font12 + left + center + FFFFFFFF + + + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(busy)) + Visible + + 840 + 465 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 915 + 521 + 90 + 38 + script.plex/busy.gif + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-video_current_playlist.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-video_current_playlist.xml new file mode 100644 index 000000000..796140288 --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-video_current_playlist.xml @@ -0,0 +1,480 @@ + + + 100 + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + + + 101 + 750 + 140 + 1170 + 800 + + 0 + 0 + 1170 + 800 + script.plex/white-square.png + B3111111 + + + 0 + 0 + 1170 + 800 + 200 + 152 + 300 + 200 + vertical + 4 + 152 + + + + 120 + 24 + + String.IsEmpty(ListItem.Property(playing)) + -10 + 0 + 60 + 100 + font10 + center + center + D8FFFFFF + + + + !String.IsEmpty(ListItem.Property(playing)) + 2 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 74 + 74 + $INFO[ListItem.Thumb] + scale + + + 168 + 0 + + 0 + 15 + 692 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 692 + 30 + font10 + left + center + B8FFFFFF + + + + + + !String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 132 + 74 + $INFO[ListItem.Thumb] + scale + + + String.IsEmpty(ListItem.Property(watched)) + 895 + -1 + 35 + 35 + script.plex/indicators/unwatched.png + + + 226 + 0 + + 0 + 15 + 584 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 584 + 30 + font10 + left + center + B8FFFFFF + + + + + + 730 + 0 + 200 + 100 + font10 + right + center + D8FFFFFF + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 98 + 930 + 2 + script.plex/white-square.png + 40000000 + + + + + + + + + !Control.HasFocus(101) + 120 + 24 + + String.IsEmpty(ListItem.Property(playing)) + -10 + 0 + 60 + 100 + font10 + center + center + D8FFFFFF + + + + !String.IsEmpty(ListItem.Property(playing)) + 0 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FFE5A00D + + + String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 74 + 74 + $INFO[ListItem.Thumb] + scale + + + 168 + 0 + + 0 + 15 + 692 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 692 + 30 + font10 + left + center + B8FFFFFF + + + + + + !String.IsEmpty(ListItem.Property(video)) + + 63 + 11 + 132 + 74 + $INFO[ListItem.Thumb] + scale + + + String.IsEmpty(ListItem.Property(watched)) + 895 + -1 + 35 + 35 + script.plex/indicators/unwatched.png + + + 226 + 0 + + 0 + 15 + 584 + 30 + font10 + left + center + FFFFFFFF + + + + 0 + 50 + 584 + 30 + font10 + left + center + B8FFFFFF + + + + + + 756 + 0 + 200 + 100 + font10 + right + center + D8FFFFFF + + + + String.IsEmpty(ListItem.Property(is.footer)) + 0 + 97 + 930 + 2 + script.plex/white-square.png + 40000000 + + + + + Control.HasFocus(101) + 63 + 21 + + -40 + -40 + 1124 + 180 + script.plex/square-rounded-shadow.png + + + 0 + 0 + 1044 + 100 + script.plex/white-square-rounded.png + FFE5A00D + + + + + String.IsEmpty(ListItem.Property(playing)) + 24 + 0 + 60 + 100 + font12 + center + center + B8000000 + + + + !String.IsEmpty(ListItem.Property(playing)) + 36 + 32.5 + 35 + 35 + script.plex/indicators/playing-circle.png + FF000000 + + + String.IsEmpty(ListItem.Property(video)) + + String.IsEmpty(ListItem.Property(video)) + 103 + 0 + 100 + 100 + $INFO[ListItem.Thumb] + scale + + + 235 + 0 + + 0 + 16 + 638 + 30 + font12 + left + center + DF000000 + + + + 0 + 51 + 638 + 30 + font10 + left + center + 98000000 + + + + + + !String.IsEmpty(ListItem.Property(video)) + + 103 + 0 + 178 + 100 + $INFO[ListItem.Thumb] + scale + + + String.IsEmpty(ListItem.Property(watched)) + 951 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + 313 + 0 + + 0 + 16 + 510 + 30 + font12 + left + center + DF000000 + + + + 0 + 51 + 510 + 30 + font10 + left + center + 98000000 + + + + + + 802 + 0 + 200 + 100 + font12 + right + center + B8000000 + + + + + + + + + + + 1128 + 33 + 10 + 734 + 101 + true + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + script.plex/white-square-rounded.png + - + - + false + vertical + false + 151 + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-video_player.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-video_player.xml new file mode 100644 index 000000000..12d2f4f3b --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-video_player.xml @@ -0,0 +1,1343 @@ + + + + 1 + 0 + 0 + + $INFO[Window.Property(background_colour)] + + + String.IsEmpty(Window.Property(use_solid_background)) + + !String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background_static)] + + + String.IsEmpty(Window.Property(use_bg_fallback)) + 0 + 0 + 1920 + 1080 + 1000 + $INFO[Window.Property(background)] + + + + !String.IsEmpty(Window.Property(post.play)) + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(post.play.background)] + + + + + Conditional + + + + + + + + + + 0 + 135 + 102 + + + 60 + 0 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + + false + 60 + 57 + 462 + 40 + font12 + left + center + A0FFFFFF + + + + + 102 + + 0 + 0 + 1920 + 580 + + 60 + 131 + + Conditional + 0 + 0 + + Control.HasFocus(101) + -45 + -45 + 552 + 349 + script.plex/drop-shadow.png + + + 0 + 0 + + 0 + 0 + 462 + 259 + $INFO[Window.Property(thumb.fallback)] + scale + + + 0 + 0 + 462 + 259 + $INFO[Window.Property(prev.thumb)] + scale + + + 193 + 91.5 + + 0 + 0 + 76 + 76 + script.plex/indicators/circle-152.png + + + 15 + 15 + 46 + 46 + script.plex/indicators/replay.png + + + + false + 0 + 269 + 462 + 35 + font10 + center + FFFFFFFF + + + + false + 0 + 301 + 462 + 35 + font10 + center + FFFFFFFF + + + + + -5 + -5 + 472 + 269 + 200 + 400 + 102 + script.plex/home/selected.png + - + + + + + + !String.IsEmpty(Window.Property(has.next)) + + false + 572 + 57 + 462 + 40 + font12 + left + center + FFFFFFFF + + + + 582 + 131 + + Conditional + 0 + 0 + + Control.HasFocus(102) + -45 + -45 + 627 + 393 + script.plex/drop-shadow.png + + + 0 + 0 + + 0 + 0 + 537 + 303 + $INFO[Window.Property(thumb.fallback)] + scale + + + 0 + 0 + 537 + 303 + $INFO[Window.Property(next.thumb)] + scale + + + !String.IsEmpty(Window.Property(countdown)) + Integer.IsGreaterOrEqual(Window.Property(countdown), 0) + 192.5 + 75.5 + + 0 + 0 + 152 + 152 + script.plex/indicators/circle-152.png + + + 8 + 8 + 136 + 136 + script.plex/circle-progress/$INFO[Window.Property(countdown)].png + + + 59.5 + 57 + 33 + 38 + script.plex/indicators/pause.png + + + + false + 0 + 313 + 537 + 30 + font10 + center + FFFFFFFF + + + + false + 0 + 345 + 537 + 30 + font10 + center + FFFFFFFF + + + + + -5 + -5 + 547 + 313 + 200 + 400 + 101 + script.plex/home/selected.png + - + + + + + + + + !String.IsEmpty(Window.Property(has.next)) + + false + 1177 + 131 + 683 + 43 + font13 + left + center + FFFFFFFF + + + + false + 1177 + 189 + 683 + 32 + font12 + left + center + A0FFFFFF + + + + false + 1177 + 300 + 683 + 215 + font12 + left + FFFFFFFF + + + + + + String.IsEmpty(Window.Property(has.next)) + + false + 580 + 131 + 1280 + 43 + font13 + left + center + FFFFFFFF + + + + false + 580 + 189 + 1280 + 32 + font12 + left + center + A0FFFFFF + + + + false + 580 + 300 + 1280 + 225 + font12 + left + FFFFFFFF + + + + + + + 0 + 585 + 1920 + 1610 + + 300 + 0 + + + Integer.IsGreater(Container(400).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 360 + 1920 + + 0 + 0 + 1920 + 360 + script.plex/white-square.png + 40000000 + + + 60 + 0 + 800 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 18 + 1920 + 430 + 100 + 401 + false + false + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 158 + + 0 + 0 + 299 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 299 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 264 + 0 + 35 + 35 + script.plex/indicators/unwatched.png + + + false + 0 + 180 + 299 + 35 + font10 + center + FFFFFFFF + + + + false + 0 + 212 + 299 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 299 + 168 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 85.5 + 20 + 128 + 128 + script.plex/home/busy.gif + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(400) + -40 + -40 + 389 + 258 + script.plex/drop-shadow.png + + + 5 + 5 + + 0 + 0 + 299 + 168 + $INFO[ListItem.Property(thumb.fallback)] + scale + + + 0 + 0 + 299 + 168 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 158 + + 0 + 0 + 299 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 299 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 264 + 0 + 35 + 35 + script.plex/indicators/unwatched.png + + + false + 0 + 180 + 299 + 35 + font10 + center + FFFFFFFF + + + + false + 0 + 212 + 299 + 35 + font10 + center + FFFFFFFF + + + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 299 + 168 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 119 + 34 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 85.5 + 20 + 128 + 128 + script.plex/home/busy.gif + + + + + Control.HasFocus(400) + 0 + 0 + 309 + 178 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(401).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 401 + 1920 + 520 + + !String.IsEmpty(Window.Property(divider.401)) + 60 + 0 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 0 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 16 + 1920 + 520 + 400 + 403 + false + false + 200 + horizontal + 4 + + + + 55 + 72 + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 72 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(401) + -40 + -40 + 324 + 441 + script.plex/drop-shadow.png + + + 5 + 5 + + !String.IsEmpty(ListItem.Property(is.boundary)) + + 0 + 0 + 244 + 361 + script.plex/white-square.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(right.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white.png + + + String.IsEmpty(ListItem.Property(is.updating)) + !String.IsEmpty(ListItem.Property(left.boundary)) + 91.5 + 130.5 + 61 + 100 + script.plex/indicators/chevron-white-l.png + + + !String.IsEmpty(ListItem.Property(is.updating)) + 58 + 116.5 + 128 + 128 + script.plex/home/busy.gif + + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Property(thumb.fallback)] + + + 0 + 0 + 244 + 361 + $INFO[ListItem.Thumb] + scale + + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 351 + + 0 + 0 + 244 + 10 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 244 + 8 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + + + !String.IsEmpty(ListItem.Property(unwatched)) + 196 + 0 + 48 + 48 + script.plex/indicators/unwatched.png + + + !String.IsEmpty(ListItem.Property(unwatched.count)) + + 193 + 0 + 51 + 39 + script.plex/white-square.png + FF000000 + + + 194 + 0 + 50 + 38 + script.plex/white-square.png + FFCC7B19 + + + 194 + 0 + 50 + 38 + font12 + center + center + FF000000 + + + + + false + 0 + 369 + 244 + 38 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(401) + 0 + 0 + 254 + 371 + script.plex/home/selected.png + + + + + + + + + Integer.IsGreater(Container(403).NumItems,0) + String.IsEmpty(Window.Property(drawing)) + 403 + 1920 + 410 + + !String.IsEmpty(Window.Property(divider.403)) + 60 + 20 + 1800 + 2 + script.plex/white-square.png + A0000000 + + + 60 + 20 + 1000 + 80 + font12 + left + center + FFFFFFFF + + + + 0 + 36 + 1920 + 410 + 401 + 404 + 200 + horizontal + 4 + + + + 55 + 61 + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + 244 + 90 + font10 + center + FFFFFFFF + + + + + + + + + + 55 + 61 + + Focus + UnFocus + 0 + 0 + + Control.HasFocus(403) + -40 + -40 + 334 + 334 + script.plex/buttons/role-shadow.png + + + 5 + 5 + + 0 + 0 + 244 + 244 + script.plex/thumb_fallbacks/role.png + + + 0 + 0 + 244 + 244 + $INFO[ListItem.Thumb] + scale + + + 0 + 253 + 244 + 90 + font10 + center + FFFFFFFF + + + + + Control.HasFocus(403) + 0 + 0 + 254 + 254 + script.plex/buttons/role-selected.png + + + + + + + + + + + Conditional + 201 + 0 + 0 + 1920 + 135 + + VisibleChange + ControlGroup(200).HasFocus(0) + !String.IsEmpty(Window.Property(on.extras)) + 0 + 0 + 1920 + 135 + script.plex/white-square.png + C0000000 + + + 60 + 47.5 + 1000 + 40 + left + 60 + horizontal + 50 + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 202 + 50 + font12 + FF000000 + script.plex/buttons/home-focus.png + script.plex/buttons/home.png + + + + + auto + 40 + font12 + left + center + FFFFFFFF + + + + 40 + 40 + + Focus + UnFocus + 40 + 40 + 204 + 201 + 50 + font12 + FF000000 + script.plex/buttons/search-focus.png + script.plex/buttons/search.png + + + + + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + 438 + 0 + + Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) + -10 + 38 + 260 + 75 + 202 + 50 + font12 + FFFFFFFF + FF000000 + right + center + script.plex/white-square-rounded.png + - + 100 + 0 + + + + 0 + 48 + 42 + 42 + $INFO[Player.Art(thumb)] + + + + !Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FFFFFFFF + MusicPlayer.Title + + + + Control.HasFocus(204) + + 53 + 48 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Artist + + + 53 + 72 + 187 + 20 + font10 + left + center + FF000000 + MusicPlayer.Title + + + + + Progressbar + 0 + 102 + 240 + 1 + script.plex/white-square-1px.png + - + script.plex/white-square-1px.png + - + - + Player.Progress + + + + 213 + 35 + 200 + 65 + font12 + right + center + FFFFFFFF + + + + 153r + 54 + 93 + 30 + script.plex/home/plex.png + + + + + !String.IsEmpty(Window.Property(search.dialog)) + + !String.IsEmpty(Window.Property(search.dialog.hasresults)) + + 0 + 0 + 1920 + 1080 + script.plex/home/background-fallback.png + + + 0 + 0 + 1920 + 1080 + $INFO[Window.Property(background)] + + + + 0 + 0 + 1920 + 1080 + script.plex/white-square.png + + + + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-video_settings_dialog.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-video_settings_dialog.xml new file mode 100644 index 000000000..4bf41907b --- /dev/null +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-video_settings_dialog.xml @@ -0,0 +1,159 @@ + + + + 1 + 0 + 0 + + + + + !String.IsEmpty(Window.Property(via.OSD)) + Hidden + 0 + 0 + + 0 + 0 + 1920 + 1080 + script.plex/player-fade.png + FF080808 + + + + !Window.IsVisible(sliderdialog) + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + 460 + 200 + + -40 + -40 + 1080 + 770 + script.plex/drop-shadow.png + + + 0 + 0 + 1000 + 80 + script.plex/white-square-top-rounded.png + F21F1F1F + + + 0 + 80 + 1000 + 610 + script.plex/white-square-top-rounded.png + F2606060 + + + 0 + 80 + 400 + 610 + script.plex/white-square-tl-rounded.png + 30000000 + + + 0 + 0 + 1000 + 80 + font12 + center + center + FFFFFFFF + + + + 0 + 80 + 990 + 600 + noop + noop + 200 + vertical + 101 + 101 + + + + 20 + 0 + 300 + 100 + font12 + left + center + FFFFFFFF + + + + 320 + 0 + 650 + 100 + font12 + right + center + FFFFFFFF + + + + + + 0 + 0 + 1000 + 100 + script.plex/white-square.png + + + 20 + 0 + 300 + 100 + font12 + left + center + FF000000 + + + + 320 + 0 + 650 + 100 + font12 + right + center + FF000000 + + + + + + + + 1450 + 280 + 10 + 600 + 101 + true + script.plex/white-square.png + script.plex/white-square.png + script.plex/white-square.png + - + - + false + vertical + false + 100 + + + + diff --git a/script.plexmod/resources/skins/Main/media/script.plex/busy-back.png b/script.plexmod/resources/skins/Main/media/script.plex/busy-back.png new file mode 100644 index 000000000..f193fb26b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/busy-back.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/busy-diffuse.png b/script.plexmod/resources/skins/Main/media/script.plex/busy-diffuse.png new file mode 100644 index 000000000..205b216d1 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/busy-diffuse.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/busy.gif b/script.plexmod/resources/skins/Main/media/script.plex/busy.gif new file mode 100644 index 000000000..d97a260ad Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/busy.gif differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/blank-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/blank-focus.png new file mode 100644 index 000000000..4b9fe6b3f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/blank-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/blank.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/blank.png new file mode 100644 index 000000000..11f18ffd5 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/blank.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/chapters-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/chapters-focus.png new file mode 100644 index 000000000..efedb9dea Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/chapters-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/chapters.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/chapters.png new file mode 100644 index 000000000..ea7d83d96 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/chapters.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/home-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/home-focus.png new file mode 100644 index 000000000..baa780c30 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/home-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/home.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/home.png new file mode 100644 index 000000000..9da048140 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/home.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/info-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/info-focus.png new file mode 100644 index 000000000..407b37b58 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/info-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/info.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/info.png new file mode 100644 index 000000000..ea0b1a344 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/info.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/media-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/media-focus.png new file mode 100644 index 000000000..a89f1c340 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/media-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/media.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/media.png new file mode 100644 index 000000000..6c69c9be2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/media.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/more-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/more-focus.png new file mode 100644 index 000000000..c814cf744 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/more-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/more-vertical.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/more-vertical.png new file mode 100644 index 000000000..4faa5ce6b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/more-vertical.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/more.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/more.png new file mode 100644 index 000000000..cfd025d39 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/more.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/next-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/next-focus.png new file mode 100644 index 000000000..b9d5e0aa3 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/next-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/next.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/next.png new file mode 100644 index 000000000..6ec144c45 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/next.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/pause-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pause-focus.png new file mode 100644 index 000000000..5d928f476 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pause-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/pause.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pause.png new file mode 100644 index 000000000..9f0083e82 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pause.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/play-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/play-focus.png new file mode 100644 index 000000000..e39c5ce15 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/play-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/play.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/play.png new file mode 100644 index 000000000..23e8c116a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/play.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/power.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/power.png new file mode 100644 index 000000000..155032cae Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/power.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/pqueue-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pqueue-focus.png new file mode 100644 index 000000000..cd23065b8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pqueue-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/pqueue.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pqueue.png new file mode 100644 index 000000000..231e9350d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/pqueue.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-focus.png new file mode 100644 index 000000000..3f86ee802 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-one-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-one-focus.png new file mode 100644 index 000000000..bc0660b51 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-one-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-one.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-one.png new file mode 100644 index 000000000..485ad9c36 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat-one.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat.png new file mode 100644 index 000000000..850245867 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/repeat.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/resume-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/resume-focus.png new file mode 100644 index 000000000..9c68899ec Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/resume-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/resume.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/resume.png new file mode 100644 index 000000000..452a67ef4 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/resume.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/role-selected.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/role-selected.png new file mode 100644 index 000000000..d06c6fc15 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/role-selected.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/role-shadow.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/role-shadow.png new file mode 100644 index 000000000..16805a373 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/role-shadow.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/rotate-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/rotate-focus.png new file mode 100644 index 000000000..43bbe7541 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/rotate-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/rotate.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/rotate.png new file mode 100644 index 000000000..2fb2730f7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/rotate.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/search-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/search-focus.png new file mode 100644 index 000000000..7d532ac56 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/search-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/search.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/search.png new file mode 100644 index 000000000..c89f56534 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/search.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/settings-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/settings-focus.png new file mode 100644 index 000000000..7af446da5 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/settings-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/settings.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/settings.png new file mode 100644 index 000000000..e24421bc0 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/settings.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/shuffle-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/shuffle-focus.png new file mode 100644 index 000000000..8dbadf6e3 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/shuffle-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/shuffle.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/shuffle.png new file mode 100644 index 000000000..a67dbde2b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/shuffle.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/skip-forward-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/skip-forward-focus.png new file mode 100644 index 000000000..a8533b2d6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/skip-forward-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/skip-forward.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/skip-forward.png new file mode 100644 index 000000000..6377f753a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/skip-forward.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/square2x2-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/square2x2-focus.png new file mode 100644 index 000000000..66f17a8c8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/square2x2-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/square2x2.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/square2x2.png new file mode 100644 index 000000000..4e2e477ea Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/square2x2.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/stop-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/stop-focus.png new file mode 100644 index 000000000..50588e55b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/stop-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/stop.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/stop.png new file mode 100644 index 000000000..1a8d4d9f7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/stop.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/subtitle-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/subtitle-focus.png new file mode 100644 index 000000000..558495060 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/subtitle-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/subtitle.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/subtitle.png new file mode 100644 index 000000000..e98064d07 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/subtitle.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/tags-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/tags-focus.png new file mode 100644 index 000000000..8eb87591d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/tags-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/tags.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/tags.png new file mode 100644 index 000000000..4ef59db7e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/tags.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/trailer-focus.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/trailer-focus.png new file mode 100644 index 000000000..57bbb48aa Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/trailer-focus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/buttons/trailer.png b/script.plexmod/resources/skins/Main/media/script.plex/buttons/trailer.png new file mode 100644 index 000000000..afd830e9a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/buttons/trailer.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/0.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/0.png new file mode 100644 index 000000000..5ef79460d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/0.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/1.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/1.png new file mode 100644 index 000000000..a4571b6ec Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/1.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/10.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/10.png new file mode 100644 index 000000000..009f5be4a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/10.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/11.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/11.png new file mode 100644 index 000000000..5faadd7c4 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/11.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/12.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/12.png new file mode 100644 index 000000000..711c177dd Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/12.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/13.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/13.png new file mode 100644 index 000000000..d841a476c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/13.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/14.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/14.png new file mode 100644 index 000000000..b0a2062d8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/14.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/15.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/15.png new file mode 100644 index 000000000..848639d40 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/15.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/2.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/2.png new file mode 100644 index 000000000..45c510f71 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/2.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/3.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/3.png new file mode 100644 index 000000000..76c1403f6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/3.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/4.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/4.png new file mode 100644 index 000000000..7b126ab2e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/4.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/5.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/5.png new file mode 100644 index 000000000..0c0212e92 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/5.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/6.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/6.png new file mode 100644 index 000000000..5b1d99739 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/6.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/7.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/7.png new file mode 100644 index 000000000..9d15832eb Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/7.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/8.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/8.png new file mode 100644 index 000000000..f221fa78d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/8.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/9.png b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/9.png new file mode 100644 index 000000000..c6885f460 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/circle-progress/9.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/drop-shadow.png b/script.plexmod/resources/skins/Main/media/script.plex/drop-shadow.png new file mode 100644 index 000000000..ca961689b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/drop-shadow.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/gray-square.png b/script.plexmod/resources/skins/Main/media/script.plex/gray-square.png new file mode 100644 index 000000000..453a5a2f8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/gray-square.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/avatar-diffuse.png b/script.plexmod/resources/skins/Main/media/script.plex/home/avatar-diffuse.png new file mode 100644 index 000000000..f64652769 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/avatar-diffuse.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/background-fallback.png b/script.plexmod/resources/skins/Main/media/script.plex/home/background-fallback.png new file mode 100644 index 000000000..4e3c9a815 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/background-fallback.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/busy.gif b/script.plexmod/resources/skins/Main/media/script.plex/home/busy.gif new file mode 100644 index 000000000..0c0828a6c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/busy.gif differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/check.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/check.png new file mode 100644 index 000000000..c6e2c3d22 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/check.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/error.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/error.png new file mode 100644 index 000000000..c60c20017 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/error.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-refreshing.gif b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-refreshing.gif new file mode 100644 index 000000000..b135ff297 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-refreshing.gif differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-secure.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-secure.png new file mode 100644 index 000000000..42d85bfc7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-secure.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-unknown.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-unknown.png new file mode 100644 index 000000000..42d85bfc7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-unknown.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-unreachable.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-unreachable.png new file mode 100644 index 000000000..c1912ca77 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/focus-unreachable.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/home.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/home.png new file mode 100644 index 000000000..d4c50b90f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/home.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/home_small.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/home_small.png new file mode 100644 index 000000000..8ce049d4b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/home_small.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/lock-24px.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/lock-24px.png new file mode 100644 index 000000000..7f08d39b4 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/lock-24px.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/lock.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/lock.png new file mode 100644 index 000000000..8242564b2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/lock.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/plex.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/plex.png new file mode 100644 index 000000000..3ed0eec57 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/plex.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/refreshing.gif b/script.plexmod/resources/skins/Main/media/script.plex/home/device/refreshing.gif new file mode 100644 index 000000000..561a16b83 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/refreshing.gif differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/secure.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/secure.png new file mode 100644 index 000000000..da0f2dab0 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/secure.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/unknown.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/unknown.png new file mode 100644 index 000000000..564c2864a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/unknown.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/device/unreachable.png b/script.plexmod/resources/skins/Main/media/script.plex/home/device/unreachable.png new file mode 100644 index 000000000..a06aa49c3 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/device/unreachable.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/plex.png b/script.plexmod/resources/skins/Main/media/script.plex/home/plex.png new file mode 100644 index 000000000..9a94e2409 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/plex.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/selected-section.png b/script.plexmod/resources/skins/Main/media/script.plex/home/selected-section.png new file mode 100644 index 000000000..9339012e6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/selected-section.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/selected.png b/script.plexmod/resources/skins/Main/media/script.plex/home/selected.png new file mode 100644 index 000000000..5f25aaaf7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/selected.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/artist.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/artist.png new file mode 100644 index 000000000..bd8e0177f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/artist.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/channels.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/channels.png new file mode 100644 index 000000000..c03c9be48 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/channels.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/home-selected.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/home-selected.png new file mode 100644 index 000000000..023557b72 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/home-selected.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/home.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/home.png new file mode 100644 index 000000000..8b94f1bdf Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/home.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/movie.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/movie.png new file mode 100644 index 000000000..5ff5f0abe Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/movie.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/photo.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/photo.png new file mode 100644 index 000000000..4d528852d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/photo.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/playlists.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/playlists.png new file mode 100644 index 000000000..b0576a1a6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/playlists.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/type/show.png b/script.plexmod/resources/skins/Main/media/script.plex/home/type/show.png new file mode 100644 index 000000000..d94060b71 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/home/type/show.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/arrow-down.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/arrow-down.png new file mode 100644 index 000000000..d70a3ed90 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/arrow-down.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/arrow-up.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/arrow-up.png new file mode 100644 index 000000000..0e3b4f480 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/arrow-up.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/busy-photo.gif b/script.plexmod/resources/skins/Main/media/script.plex/indicators/busy-photo.gif new file mode 100644 index 000000000..70ffff8fa Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/busy-photo.gif differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/camera.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/camera.png new file mode 100644 index 000000000..4f3b7b4e5 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/camera.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/chevron-white-l.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/chevron-white-l.png new file mode 100644 index 000000000..45a6bcbee Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/chevron-white-l.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/chevron-white.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/chevron-white.png new file mode 100644 index 000000000..3f1d8f0f2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/chevron-white.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/circle-152.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/circle-152.png new file mode 100644 index 000000000..b1862c0f0 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/circle-152.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/circle-19.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/circle-19.png new file mode 100644 index 000000000..9d1a852dc Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/circle-19.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/cornerbox.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/cornerbox.png new file mode 100644 index 000000000..510dfb891 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/cornerbox.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/dropdown-triangle.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/dropdown-triangle.png new file mode 100644 index 000000000..ce90adcb8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/dropdown-triangle.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/info-sep.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/info-sep.png new file mode 100644 index 000000000..b55e87c5b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/info-sep.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/pause.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/pause.png new file mode 100644 index 000000000..622f04c76 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/pause.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time.png new file mode 100644 index 000000000..80ff25d9b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time_arrow.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time_arrow.png new file mode 100644 index 000000000..fe5e15a2e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time_arrow.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time_box.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time_box.png new file mode 100644 index 000000000..5f2e25c92 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/player-selection-time_box.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/playing-circle.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/playing-circle.png new file mode 100644 index 000000000..e02072070 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/playing-circle.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/remove.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/remove.png new file mode 100644 index 000000000..bfc5c3514 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/remove.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/replay.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/replay.png new file mode 100644 index 000000000..ac7cbe2e0 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/replay.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/seek-selection-marker.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/seek-selection-marker.png new file mode 100644 index 000000000..d650a89b9 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/seek-selection-marker.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/spinner.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/spinner.png new file mode 100644 index 000000000..6ddb4d4ac Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/spinner.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/unwatched-rounded.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/unwatched-rounded.png new file mode 100644 index 000000000..0391998eb Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/unwatched-rounded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/indicators/unwatched.png b/script.plexmod/resources/skins/Main/media/script.plex/indicators/unwatched.png new file mode 100644 index 000000000..106c18bae Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/indicators/unwatched.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/masks/listview-16x9-backgroundmask.png b/script.plexmod/resources/skins/Main/media/script.plex/masks/listview-16x9-backgroundmask.png new file mode 100644 index 000000000..092aa9d2e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/masks/listview-16x9-backgroundmask.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/masks/role.png b/script.plexmod/resources/skins/Main/media/script.plex/masks/role.png new file mode 100644 index 000000000..302d405cf Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/masks/role.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/player-fade.png b/script.plexmod/resources/skins/Main/media/script.plex/player-fade.png new file mode 100644 index 000000000..c84d4b604 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/player-fade.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/0.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/0.png new file mode 100644 index 000000000..4ff39bf62 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/0.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/10.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/10.png new file mode 100644 index 000000000..e948275c9 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/10.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/100.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/100.png new file mode 100644 index 000000000..b25f1dc57 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/100.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/12.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/12.png new file mode 100644 index 000000000..3cff59469 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/12.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/14.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/14.png new file mode 100644 index 000000000..fef67aee2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/14.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/16.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/16.png new file mode 100644 index 000000000..2ae7f6909 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/16.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/18.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/18.png new file mode 100644 index 000000000..92f3b3af8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/18.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/2.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/2.png new file mode 100644 index 000000000..50c974ff7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/2.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/20.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/20.png new file mode 100644 index 000000000..c799b4c6d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/20.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/22.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/22.png new file mode 100644 index 000000000..b79551d56 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/22.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/24.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/24.png new file mode 100644 index 000000000..7083c4623 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/24.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/26.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/26.png new file mode 100644 index 000000000..d4d13e887 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/26.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/28.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/28.png new file mode 100644 index 000000000..dfc2b164a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/28.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/30.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/30.png new file mode 100644 index 000000000..a0c9aa4b8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/30.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/32.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/32.png new file mode 100644 index 000000000..e4224f7b8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/32.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/34.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/34.png new file mode 100644 index 000000000..08a0b78c9 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/34.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/36.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/36.png new file mode 100644 index 000000000..74889d156 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/36.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/38.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/38.png new file mode 100644 index 000000000..c653854d7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/38.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/4.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/4.png new file mode 100644 index 000000000..06aaed9b7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/4.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/40.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/40.png new file mode 100644 index 000000000..02bae0233 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/40.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/42.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/42.png new file mode 100644 index 000000000..a5259c967 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/42.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/44.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/44.png new file mode 100644 index 000000000..72a4f4c13 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/44.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/46.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/46.png new file mode 100644 index 000000000..6e7155e69 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/46.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/48.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/48.png new file mode 100644 index 000000000..42a61770e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/48.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/50.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/50.png new file mode 100644 index 000000000..0e0934d22 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/50.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/52.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/52.png new file mode 100644 index 000000000..353fa3678 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/52.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/54.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/54.png new file mode 100644 index 000000000..66cae819e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/54.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/56.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/56.png new file mode 100644 index 000000000..7f806034c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/56.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/58.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/58.png new file mode 100644 index 000000000..2aad245a9 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/58.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/6.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/6.png new file mode 100644 index 000000000..56ce81719 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/6.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/60.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/60.png new file mode 100644 index 000000000..3e299ed84 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/60.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/62.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/62.png new file mode 100644 index 000000000..8adb561c0 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/62.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/64.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/64.png new file mode 100644 index 000000000..228dba450 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/64.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/66.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/66.png new file mode 100644 index 000000000..322c26e43 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/66.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/68.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/68.png new file mode 100644 index 000000000..0c853000b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/68.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/70.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/70.png new file mode 100644 index 000000000..869f9d751 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/70.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/72.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/72.png new file mode 100644 index 000000000..e9158e628 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/72.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/74.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/74.png new file mode 100644 index 000000000..f8e02419f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/74.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/76.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/76.png new file mode 100644 index 000000000..d99989e92 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/76.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/78.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/78.png new file mode 100644 index 000000000..2333dba5c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/78.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/8.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/8.png new file mode 100644 index 000000000..12472cb92 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/8.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/80.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/80.png new file mode 100644 index 000000000..873fdc264 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/80.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/82.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/82.png new file mode 100644 index 000000000..4f40e6ba6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/82.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/84.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/84.png new file mode 100644 index 000000000..86a77bcee Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/84.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/86.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/86.png new file mode 100644 index 000000000..bd9404700 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/86.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/88.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/88.png new file mode 100644 index 000000000..b5c34a4d1 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/88.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/90.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/90.png new file mode 100644 index 000000000..528fc9887 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/90.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/92.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/92.png new file mode 100644 index 000000000..565e15edf Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/92.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/94.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/94.png new file mode 100644 index 000000000..5653ff0cb Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/94.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/96.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/96.png new file mode 100644 index 000000000..6bfbd5ccc Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/96.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/progress/98.png b/script.plexmod/resources/skins/Main/media/script.plex/progress/98.png new file mode 100644 index 000000000..60579118f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/progress/98.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/imdb/image.rating.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/imdb/image.rating.png new file mode 100644 index 000000000..adce94124 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/imdb/image.rating.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/other/image.rating.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/other/image.rating.png new file mode 100644 index 000000000..a5f83e717 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/other/image.rating.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.certified.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.certified.png new file mode 100644 index 000000000..0607c9c11 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.certified.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.plus.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.plus.png new file mode 100644 index 000000000..57758c6a2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.plus.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.ripe.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.ripe.png new file mode 100644 index 000000000..5ad9dffb3 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.ripe.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.rotten.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.rotten.png new file mode 100644 index 000000000..2b7c9bcf4 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.rotten.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.spilled.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.spilled.png new file mode 100644 index 000000000..0f89c619e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.spilled.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.upright.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.upright.png new file mode 100644 index 000000000..42597e48f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/rottentomatoes/image.rating.upright.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/ratings/tmdb/image.rating.png b/script.plexmod/resources/skins/Main/media/script.plex/ratings/tmdb/image.rating.png new file mode 100644 index 000000000..cc94da93f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/ratings/tmdb/image.rating.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/reviews/image.review.fresh.png b/script.plexmod/resources/skins/Main/media/script.plex/reviews/image.review.fresh.png new file mode 100644 index 000000000..a18fb601a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/reviews/image.review.fresh.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/reviews/image.review.rotten.png b/script.plexmod/resources/skins/Main/media/script.plex/reviews/image.review.rotten.png new file mode 100644 index 000000000..39f687008 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/reviews/image.review.rotten.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/section_type/movie.png b/script.plexmod/resources/skins/Main/media/script.plex/section_type/movie.png new file mode 100644 index 000000000..57baf25d0 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/section_type/movie.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/section_type/music.png b/script.plexmod/resources/skins/Main/media/script.plex/section_type/music.png new file mode 100644 index 000000000..5ec8469f6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/section_type/music.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/section_type/photo.png b/script.plexmod/resources/skins/Main/media/script.plex/section_type/photo.png new file mode 100644 index 000000000..814e233cd Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/section_type/photo.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/section_type/show.png b/script.plexmod/resources/skins/Main/media/script.plex/section_type/show.png new file mode 100644 index 000000000..434a46f20 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/section_type/show.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/settings/checkbox.png b/script.plexmod/resources/skins/Main/media/script.plex/settings/checkbox.png new file mode 100644 index 000000000..8fbec4ff4 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/settings/checkbox.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/settings/checkmark.png b/script.plexmod/resources/skins/Main/media/script.plex/settings/checkmark.png new file mode 100644 index 000000000..641a45902 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/settings/checkmark.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/settings/expanded.png b/script.plexmod/resources/skins/Main/media/script.plex/settings/expanded.png new file mode 100644 index 000000000..801bfa80b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/settings/expanded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/back.jpg b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/back.jpg new file mode 100644 index 000000000..5ea94ee01 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/back.jpg differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/0.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/0.png new file mode 100644 index 000000000..e04b08334 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/0.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/1.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/1.png new file mode 100644 index 000000000..75318efd6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/1.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/2.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/2.png new file mode 100644 index 000000000..f3a4f6406 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/2.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/3.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/3.png new file mode 100644 index 000000000..21897e381 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/3.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/4.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/4.png new file mode 100644 index 000000000..f3bb0fe77 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/4.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/5.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/5.png new file mode 100644 index 000000000..d239ea976 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/5.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/6.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/6.png new file mode 100644 index 000000000..061359615 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/6.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/7.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/7.png new file mode 100644 index 000000000..a9e573d20 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/7.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/8.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/8.png new file mode 100644 index 000000000..d4f827e6e Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/8.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/9.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/9.png new file mode 100644 index 000000000..1228b3cb1 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/9.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/A.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/A.png new file mode 100644 index 000000000..f674ffbb1 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/A.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/B.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/B.png new file mode 100644 index 000000000..c31c541e4 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/B.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/C.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/C.png new file mode 100644 index 000000000..551976f50 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/C.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/D.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/D.png new file mode 100644 index 000000000..fcfa04f79 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/D.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/E.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/E.png new file mode 100644 index 000000000..565e1ec3b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/E.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/F.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/F.png new file mode 100644 index 000000000..fd23c2b46 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/F.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/G.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/G.png new file mode 100644 index 000000000..e0a458439 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/G.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/H.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/H.png new file mode 100644 index 000000000..5f8e41fe2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/H.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/I.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/I.png new file mode 100644 index 000000000..8a54bccc2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/I.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/J.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/J.png new file mode 100644 index 000000000..e0c6c3a98 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/J.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/K.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/K.png new file mode 100644 index 000000000..6d99e483c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/K.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/L.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/L.png new file mode 100644 index 000000000..434413add Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/L.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/M.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/M.png new file mode 100644 index 000000000..c15ebec1d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/M.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/N.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/N.png new file mode 100644 index 000000000..2e00eedba Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/N.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/O.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/O.png new file mode 100644 index 000000000..c5b01dc2c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/O.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/P.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/P.png new file mode 100644 index 000000000..2a30131bf Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/P.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Q.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Q.png new file mode 100644 index 000000000..672ec0159 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Q.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/R.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/R.png new file mode 100644 index 000000000..438182e5c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/R.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/S.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/S.png new file mode 100644 index 000000000..ddf3373e8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/S.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/T.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/T.png new file mode 100644 index 000000000..a5a467b48 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/T.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/U.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/U.png new file mode 100644 index 000000000..bef0ab8d6 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/U.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/V.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/V.png new file mode 100644 index 000000000..520ccf741 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/V.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/W.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/W.png new file mode 100644 index 000000000..8407f61dd Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/W.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/X.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/X.png new file mode 100644 index 000000000..53e839410 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/X.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Y.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Y.png new file mode 100644 index 000000000..49da48a18 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Y.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Z.png b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Z.png new file mode 100644 index 000000000..64e62120b Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/digits/Z.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/generating-code.jpg b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/generating-code.jpg new file mode 100644 index 000000000..84bc808b7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/generating-code.jpg differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/linking-account.jpg b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/linking-account.jpg new file mode 100644 index 000000000..6a6618ad2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/linking-account.jpg differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/pin-display.jpg b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/pin-display.jpg new file mode 100644 index 000000000..cee0e3590 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/pin-display.jpg differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/plexpass.jpg b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/plexpass.jpg new file mode 100644 index 000000000..2e08355e8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/plexpass.jpg differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/pre-signin.jpg b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/pre-signin.jpg new file mode 100644 index 000000000..6b1f9ac54 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/pre-signin.jpg differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/sign_in/refresh-code.jpg b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/refresh-code.jpg new file mode 100644 index 000000000..8af7a04c2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/sign_in/refresh-code.jpg differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/splash.png b/script.plexmod/resources/skins/Main/media/script.plex/splash.png new file mode 100644 index 000000000..e7a2a53ac Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/splash.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/square-rounded-shadow.png b/script.plexmod/resources/skins/Main/media/script.plex/square-rounded-shadow.png new file mode 100644 index 000000000..e22480fde Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/square-rounded-shadow.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/stars/0.png b/script.plexmod/resources/skins/Main/media/script.plex/stars/0.png new file mode 100644 index 000000000..fd047749a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/stars/0.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/stars/1.png b/script.plexmod/resources/skins/Main/media/script.plex/stars/1.png new file mode 100644 index 000000000..a338d5a02 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/stars/1.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/stars/2.png b/script.plexmod/resources/skins/Main/media/script.plex/stars/2.png new file mode 100644 index 000000000..b60232cc8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/stars/2.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/stars/3.png b/script.plexmod/resources/skins/Main/media/script.plex/stars/3.png new file mode 100644 index 000000000..eec791c98 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/stars/3.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/stars/4.png b/script.plexmod/resources/skins/Main/media/script.plex/stars/4.png new file mode 100644 index 000000000..b90e683bb Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/stars/4.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/stars/5.png b/script.plexmod/resources/skins/Main/media/script.plex/stars/5.png new file mode 100644 index 000000000..ab646cf5c Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/stars/5.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/broken-photo-thumb.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/broken-photo-thumb.png new file mode 100644 index 000000000..fbe365038 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/broken-photo-thumb.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/broken-photo.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/broken-photo.png new file mode 100644 index 000000000..b4330de71 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/broken-photo.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/movie.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/movie.png new file mode 100644 index 000000000..e847163b8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/movie.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/movie16x9.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/movie16x9.png new file mode 100644 index 000000000..7c2c92942 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/movie16x9.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/music.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/music.png new file mode 100644 index 000000000..9fb7042f7 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/music.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/photo.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/photo.png new file mode 100644 index 000000000..ecaff1e92 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/photo.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/role.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/role.png new file mode 100644 index 000000000..a3f4c0995 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/role.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/show.png b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/show.png new file mode 100644 index 000000000..b4eecad88 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/thumb_fallbacks/show.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/transparent-6px.png b/script.plexmod/resources/skins/Main/media/script.plex/transparent-6px.png new file mode 100644 index 000000000..12269aa19 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/transparent-6px.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/admin-back.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/admin-back.png new file mode 100644 index 000000000..2335b53bc Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/admin-back.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/admin-icon.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/admin-icon.png new file mode 100644 index 000000000..f98f65136 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/admin-icon.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/avatar-background.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/avatar-background.png new file mode 100644 index 000000000..03926d8f2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/avatar-background.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/avatar-diffuse.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/avatar-diffuse.png new file mode 100644 index 000000000..b375889ef Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/avatar-diffuse.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/backspace.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/backspace.png new file mode 100644 index 000000000..c5324296f Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/backspace.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/backspace_nf.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/backspace_nf.png new file mode 100644 index 000000000..9b6dd0bf2 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/backspace_nf.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background-bottom.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background-bottom.png new file mode 100644 index 000000000..f39ea934a Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background-bottom.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background-top.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background-top.png new file mode 100644 index 000000000..803c7da69 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background-top.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background.png new file mode 100644 index 000000000..82f2aa605 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/item-background.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/plex.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/plex.png new file mode 100644 index 000000000..2fbd92adf Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/plex.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/protected-back.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/protected-back.png new file mode 100644 index 000000000..a318164bd Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/protected-back.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/protected-icon.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/protected-icon.png new file mode 100644 index 000000000..d6188176d Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/user_select/protected-icon.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-outline-rounded.png b/script.plexmod/resources/skins/Main/media/script.plex/white-outline-rounded.png new file mode 100644 index 000000000..3ebd26af8 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-outline-rounded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-1px.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-1px.png new file mode 100644 index 000000000..d81bd9903 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-1px.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-6px.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-6px.png new file mode 100644 index 000000000..5a1083bec Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-6px.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-left-rounded.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-left-rounded.png new file mode 100644 index 000000000..ab2aba6fa Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-left-rounded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-4r.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-4r.png new file mode 100644 index 000000000..5617eda80 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-4r.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-top-padded.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-top-padded.png new file mode 100644 index 000000000..d886c3b15 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-top-padded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-with-shadow.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-with-shadow.png new file mode 100644 index 000000000..4bab599d3 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded-with-shadow.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded.png new file mode 100644 index 000000000..c73f7a7fb Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-rounded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-tl-rounded.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-tl-rounded.png new file mode 100644 index 000000000..a02720a79 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-tl-rounded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square-top-rounded.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square-top-rounded.png new file mode 100644 index 000000000..c527e78b3 Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square-top-rounded.png differ diff --git a/script.plexmod/resources/skins/Main/media/script.plex/white-square.png b/script.plexmod/resources/skins/Main/media/script.plex/white-square.png new file mode 100644 index 000000000..52065d4ec Binary files /dev/null and b/script.plexmod/resources/skins/Main/media/script.plex/white-square.png differ diff --git a/script.plexmod/resources/skins/Main/skin.xml b/script.plexmod/resources/skins/Main/skin.xml new file mode 100644 index 000000000..757461a68 --- /dev/null +++ b/script.plexmod/resources/skins/Main/skin.xml @@ -0,0 +1,14 @@ + + + 1080i + 1080i + 1.0 + + + 0 + + + Main + Plex + + \ No newline at end of file diff --git a/script.plexmod/screensaver.py b/script.plexmod/screensaver.py new file mode 100644 index 000000000..8e15051bc --- /dev/null +++ b/script.plexmod/screensaver.py @@ -0,0 +1,24 @@ +import xbmc + +from lib import plex, util + +from lib.windows import slidehshow + +class ScreensaverMonitor(xbmc.Monitor): + def __init__( self, *args, **kwargs ): + self.action = kwargs['action'] + + def onScreensaverDeactivated(self): + self.action() + +def main(): + util.DEBUG_LOG("[SS] Starting") + if plex.init(): + with util.Cron(1): + ss = slidehshow.Slideshow.create() + ss.monitor = ScreensaverMonitor(action = ss.close) + ss.modal() + del ss + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/script.plexmod/service.py b/script.plexmod/service.py new file mode 100644 index 000000000..1698e9783 --- /dev/null +++ b/script.plexmod/service.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from kodi_six import xbmc +from kodi_six import xbmcgui +from kodi_six import xbmcaddon + + +def main(): + if xbmc.getInfoLabel('Window(10000).Property(script.plex.service.started)'): + # Prevent add-on updates from starting a new version of the addon + return + + xbmcgui.Window(10000).setProperty('script.plex.service.started', '1') + + if xbmcaddon.Addon().getSetting('kiosk.mode') == 'true': + xbmc.log('script.plex: Starting from service (Kiosk Mode)', xbmc.LOGINFO) + xbmc.executebuiltin('RunScript(script.plexmod)') + + +if __name__ == '__main__': + main() diff --git a/script.radioparadise/CHANGELOG.md b/script.radioparadise/CHANGELOG.md deleted file mode 100644 index d336bef27..000000000 --- a/script.radioparadise/CHANGELOG.md +++ /dev/null @@ -1,29 +0,0 @@ -## v2.0.0 - -- Updates for Kodi v20 -- Improve metadata handling - -## v1.0.5 - -- Rename World/Etc mix to Global - -## v1.0.4 - -- Update logos -- Improve metadata error handling - -## v1.0.3 - -- Add mix selection by script parameter - -## v1.0.2 - -- Handle missing stream metadata - -## v1.0.1 - -- Restart playback automatically - -## v1.0.0 - -- Initial release diff --git a/script.radioparadise/COPYING b/script.radioparadise/COPYING deleted file mode 100644 index f288702d2..000000000 --- a/script.radioparadise/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -them 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 prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey 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; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - 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. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -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. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/script.radioparadise/LICENSE.txt b/script.radioparadise/LICENSE.txt deleted file mode 100644 index 9d7514d02..000000000 --- a/script.radioparadise/LICENSE.txt +++ /dev/null @@ -1,16 +0,0 @@ -Radio Paradise logos and photos: Copyright Radio Paradise - -All other files ("This program"): Copyright Alexander Dietrich - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . diff --git a/script.radioparadise/README.md b/script.radioparadise/README.md deleted file mode 100644 index 1a3c8b352..000000000 --- a/script.radioparadise/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Radio Paradise addon for Kodi - -Plays [Radio Paradise][] music mixes, accompanied by the HD slideshow. - -[radio paradise]: https://radioparadise.com/ - -## Features - -- Radio Paradise music mixes in AAC or FLAC -- HD slideshow (optional) -- Auto Play (optional) - -## Requirements - -- Kodi [release][] v20 or later - -[release]: https://kodi.wiki/view/Releases - -## Mix Selection by Script Parameter - -In addition to the Auto Play feature, the addon script can be called with a -parameter to start a particular RP mix: - -```python -RunScript('script.radioparadise', 1) -``` - -Mix parameter: - -| Value | Mix | -| --- | --- | -| 0 | RP Main Mix | -| 1 | RP Mellow Mix | -| 2 | RP Rock Mix | -| 3 | RP Global Mix | - -This can be used to add shortcuts for RP mixes in [favourites.xml][]. - -[favourites.xml]: https://kodi.wiki/view/Favourites.xml diff --git a/script.radioparadise/addon.xml b/script.radioparadise/addon.xml deleted file mode 100644 index e32e7f783..000000000 --- a/script.radioparadise/addon.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - audio - - - - Radio Paradise addon for Kodi - An eclectic DJ-mixed blend of rock, indie, electronica, world music, and more. Listener supported & always 100% commercial free. - https://radioparadise.com/ - https://github.com/alxndr42/script.radioparadise - GPL-3.0-or-later - all - - resources/icon.png - resources/fanart.jpg - resources/clearlogo.png - - - diff --git a/script.radioparadise/resources/clearlogo.png b/script.radioparadise/resources/clearlogo.png deleted file mode 100644 index c8bb65936..000000000 Binary files a/script.radioparadise/resources/clearlogo.png and /dev/null differ diff --git a/script.radioparadise/resources/fanart.jpg b/script.radioparadise/resources/fanart.jpg deleted file mode 100644 index a3f762709..000000000 Binary files a/script.radioparadise/resources/fanart.jpg and /dev/null differ diff --git a/script.radioparadise/resources/icon.png b/script.radioparadise/resources/icon.png deleted file mode 100644 index c4d739488..000000000 Binary files a/script.radioparadise/resources/icon.png and /dev/null differ diff --git a/script.radioparadise/resources/language/resource.language.en_gb/strings.po b/script.radioparadise/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index ca9085e18..000000000 --- a/script.radioparadise/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,46 +0,0 @@ -msgid "" -msgstr "" - -msgctxt "#30101" -msgid "Audio Format" -msgstr "" - -msgctxt "#30102" -msgid "Auto Play" -msgstr "" - -msgctxt "#30103" -msgid "Visuals" -msgstr "" - -msgctxt "#30104" -msgid "Slide Duration (seconds)" -msgstr "" - -msgctxt "#30201" -msgid "AAC" -msgstr "" - -msgctxt "#30202" -msgid "FLAC" -msgstr "" - -msgctxt "#30203" -msgid "RP Main Mix" -msgstr "" - -msgctxt "#30204" -msgid "RP Mellow Mix" -msgstr "" - -msgctxt "#30205" -msgid "RP Rock Mix" -msgstr "" - -msgctxt "#30206" -msgid "RP Global Mix" -msgstr "" - -msgctxt "#30207" -msgid "RP Slideshow" -msgstr "" diff --git a/script.radioparadise/resources/lib/__init__.py b/script.radioparadise/resources/lib/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.radioparadise/resources/lib/radioparadise.py b/script.radioparadise/resources/lib/radioparadise.py deleted file mode 100644 index ce5f31df7..000000000 --- a/script.radioparadise/resources/lib/radioparadise.py +++ /dev/null @@ -1,142 +0,0 @@ -from collections import OrderedDict -import re -import time - -import requests - - -NOWPLAYING_URL = 'https://api.radioparadise.com/api/nowplaying_list?chan={}' -COVER_URL = 'https://img.radioparadise.com/{}' -SLIDESHOW_URL = 'https://img.radioparadise.com/slideshow/720/{}.jpg' - -BREAK_COVER_URL = 'https://img.radioparadise.com/covers/l/101.jpg' -BREAK_SONG = ('Commercial-free', 'Listener-supported') - -KEY_FILTER_RE = re.compile(r'[^\w\']+') - -# Number of songs to cache -MAX_SONGS = 30 -# Number of seconds to wait for API responses -UPDATE_TIMEOUT = 3 -# Number of seconds to wait before retrying API updates -UPDATE_WAIT = 5 - -STREAMS = [ - { - 'channel': 0, - 'title': 'RP Main Mix', - 'url_aac': 'http://stream.radioparadise.com/aac-128', - 'url_flac': 'http://stream.radioparadise.com/flacm', - }, - { - 'channel': 1, - 'title': 'RP Mellow Mix', - 'url_aac': 'http://stream.radioparadise.com/mellow-128', - 'url_flac': 'http://stream.radioparadise.com/mellow-flacm', - }, - { - 'channel': 2, - 'title': 'RP Rock Mix', - 'url_aac': 'http://stream.radioparadise.com/rock-128', - 'url_flac': 'http://stream.radioparadise.com/rock-flacm', - }, - { - 'channel': 3, - 'title': 'RP Global Mix', - 'url_aac': 'http://stream.radioparadise.com/global-128', - 'url_flac': 'http://stream.radioparadise.com/global-flacm', - }, -] -STREAM_INFO = {s['url_aac']: s for s in STREAMS} -STREAM_INFO.update({s['url_flac']: s for s in STREAMS}) - - -class NowPlaying(): - """Provides song information from the "nowplaying" API.""" - - def __init__(self): - """Constructor""" - self.songs = OrderedDict() - self.set_channel(None) - - def get_song_data(self, song_key): - """Return a dict for the build_key()-created key, or None. - - The "cover" value will be an absolute URL. - """ - return self.songs.get(song_key) - - def get_next_song(self, song_key): - """Return a dict for song_key's successor, or None. - - The "cover" value will be an absolute URL. - """ - next_key = self.songs.get(song_key, {}).get('next_key') - return self.songs.get(next_key) - - def set_channel(self, channel): - """Set the RP channel number, or None.""" - if channel is not None: - self.url = NOWPLAYING_URL.format(channel) - else: - self.url = None - self.current = None - self.next_update = 0 - self.songs.clear() - - def update(self): - """Update song information from the API, if necessary. - - Calls the API only if the "refresh" timer has expired. - - Raises an exception on error responses or timeouts. - """ - if self.url is None: - return - if time.time() < self.next_update: - return - - try: - res = requests.get(self.url, timeout=UPDATE_TIMEOUT) - res.raise_for_status() - except Exception: - self.next_update = time.time() + UPDATE_WAIT - raise - - next_key = None - data = res.json() - song_items = sorted(list(data['song'].items()), key=lambda s: int(s[0])) - for index, song in song_items: - if song['artist'] is None: - song['artist'] = 'Unknown Artist' - if song['title'] is None: - song['title'] = 'Unknown Title' - song['cover'] = COVER_URL.format(song['cover']) - slides = song.get('slideshow', '').split(',') - slides = [SLIDESHOW_URL.format(s) for s in slides if s] - song['slide_urls'] = slides - song['next_key'] = next_key - key = build_key((song['artist'], song['title'])) - self.songs[key] = song - next_key = key - if index == '0': - self.current = song - if (break_key := build_key(BREAK_SONG)) not in self.songs: - self.songs[break_key] = { - 'artist': BREAK_SONG[0], - 'title': BREAK_SONG[1], - 'cover': BREAK_COVER_URL, - 'duration': '30000', - } - self.next_update = time.time() + data['refresh'] - while len(self.songs) > MAX_SONGS: - self.songs.popitem(last=False) - - -def build_key(strings): - """Return a normalized tuple of words in the strings.""" - result = [] - for s in strings: - words = KEY_FILTER_RE.sub(' ', s).casefold().split() - result.extend(words) - return tuple(sorted(result)) diff --git a/script.radioparadise/resources/lib/script.py b/script.radioparadise/resources/lib/script.py deleted file mode 100644 index a7206833f..000000000 --- a/script.radioparadise/resources/lib/script.py +++ /dev/null @@ -1,58 +0,0 @@ -import sys - -import xbmc -import xbmcaddon -import xbmcgui - -from .radioparadise import STREAMS - - -class Window(xbmcgui.WindowXML): - def onInit(self): - xbmc.executebuiltin('Container.SetViewMode(100)') - listitems = [] - for s in STREAMS: - item = xbmcgui.ListItem(s['title']) - item.setProperty('channel', str(s['channel'])) - listitems.append(item) - self.clearList() - self.addItems(listitems) - xbmc.sleep(100) - self.setFocusId(self.getCurrentContainerId()) - - def onClick(self, controlId): - if controlId == 100: - item = self.getListItem(self.getCurrentListPosition()) - channel = int(item.getProperty('channel')) - play_channel(channel) - self.close() - - -def play_channel(channel_number): - """Play the channel, unless it's already playing.""" - stream = STREAMS[channel_number] - addon = xbmcaddon.Addon() - audio_format = addon.getSetting('audio_format') - if audio_format == 'flac': - url = stream['url_flac'] - else: - url = stream['url_aac'] - player = xbmc.Player() - if not player.isPlayingAudio() or player.getPlayingFile() != url: - player.stop() - player.play(url) - - -def run_script(): - addon = xbmcaddon.Addon() - if len(sys.argv) == 2: - auto_play = int(sys.argv[1]) - else: - auto_play = addon.getSettingInt('auto_play') - if auto_play == -1: - addon_path = addon.getAddonInfo('path') - window = Window('script-radioparadise.xml', addon_path) - window.doModal() - del window - else: - play_channel(auto_play) diff --git a/script.radioparadise/resources/lib/service.py b/script.radioparadise/resources/lib/service.py deleted file mode 100644 index c96612c39..000000000 --- a/script.radioparadise/resources/lib/service.py +++ /dev/null @@ -1,285 +0,0 @@ -import time -import traceback - -import requests -import xbmc -import xbmcaddon -import xbmcgui - -from .radioparadise import STREAM_INFO, NowPlaying, build_key - - -DEVELOPMENT = False - -EXPIRATION_DELAY = 10 - -RESTART_DELAY = 1.0 -RESTART_TIMEOUT = 1.0 - - -class Song(): - """Current song information.""" - - def __init__(self, key, data, fanart, start_time): - self.key = key - self.data = data - self.fanart = fanart - self.cover = data['cover'] - self.start_time = start_time - self.duration = int(data['duration']) / 1000 - - def __str__(self): - artist = self.data.get('artist', 'Unknown Artist') - title = self.data.get('title', 'Unknown Title') - return f'{artist} - {title}' - - def expired(self): - """Return True if this Song should be considered overdue.""" - if self.start_time: - expiration = self.start_time + self.duration + EXPIRATION_DELAY - return time.time() > expiration - else: - return False - - -class Player(xbmc.Player): - """Adds xbmc.Player callbacks and integrates with the RP API.""" - - def __init__(self): - """Constructor""" - super().__init__() - self.song = None - self.stream_url = None - self.restart_time = 0 - self.tracked_key = None - self.tracked_time = 0 - self.now_playing = NowPlaying() - self.slideshow = Slideshow() - - def get_song_key(self): - """Return a key for the current song, or None.""" - result = None - if self.isPlayingAudio(): - try: - info = self.getMusicInfoTag() - artist_title = (info.getArtist(), info.getTitle()) - if artist_title != ('', ''): - result = build_key(artist_title) - except Exception: - pass - return result - - def reset(self): - """Reset internal state when not playing RP.""" - self.song = None - self.stream_url = None - self.restart_time = 0 - self.tracked_key = None - self.tracked_time = 0 - self.now_playing.set_channel(None) - self.slideshow.set_slides(None) - - def restart(self): - """Restart playback, if necessary.""" - if not self.restart_time or time.time() < self.restart_time: - return - try: - res = requests.head(self.stream_url, timeout=RESTART_TIMEOUT) - do_restart = res.status_code == 200 - except Exception: - do_restart = False - if do_restart: - self.restart_time = 0 - self.play(self.stream_url) - else: - self.restart_time = time.time() + RESTART_DELAY - - def update(self): - """Perform updates.""" - if self.restart_time: - self.restart() - elif self.stream_url: - self.now_playing.update() - self.update_slideshow() - self.update_song() - - def update_player(self): - """Update the Kodi player with song metadata.""" - song = self.song - if song and self.isPlayingAudio(): - item = self.getPlayingItem() - tag = item.getMusicInfoTag() - tag.setArtist(song.data['artist']) - tag.setTitle(song.data['title']) - tag.setGenres([]) - tag.setAlbum(song.data.get('album', '')) - rating = float(song.data.get('rating', 0)) - tag.setRating(rating) - tag.setUserRating(int(round(rating))) - tag.setYear(int(song.data.get('year', 0))) - item.setArt({'thumb': song.cover}) - item.setArt({'fanart': song.fanart}) - self.updateInfoTag(item) - - def clear_player(self): - """Clear most of the Kodi player's song information.""" - if self.isPlayingAudio(): - info = self.getMusicInfoTag() - item = self.getPlayingItem() - tag = item.getMusicInfoTag() - tag.setArtist(info.getArtist()) - tag.setTitle(info.getTitle()) - tag.setGenres([]) - tag.setAlbum('') - tag.setRating(0) - tag.setUserRating(0) - tag.setYear(0) - item.setArt({'thumb': None}) - item.setArt({'fanart': None}) - self.updateInfoTag(item) - - def update_slideshow(self): - """Update the slideshow, if necessary.""" - song = self.song - next_slide = self.slideshow.next_slide() - if song and next_slide: - song.fanart = next_slide - self.update_player() - - def update_song(self): - """Update song metadata, if necessary.""" - player_key = self.get_song_key() - song = self.song - if player_key is None: - return - if song and not (song.key != player_key or song.expired()): - return - - # Keep track of the local song start time - if self.tracked_key != player_key: - if self.tracked_key is not None: - self.tracked_time = time.time() - self.tracked_key = player_key - - start_time = None - song_data = None - # Try to match API metadata on song changes - if song and song.key != player_key and not song.expired(): - start_time = self.tracked_time - song_data = self.now_playing.get_song_data(player_key) - # Show "next" song if the song change was missed - elif song and song.expired(): - start_time = song.start_time + song.duration - song_data = self.now_playing.get_next_song(player_key) - # Without API metadata, show the stream metadata - if song_data is None and song.start_time: - song.start_time = 0 - self.slideshow.set_slides(None) - self.clear_player() - # Show "current" song after starting playback - elif song is None: - song_data = self.now_playing.current - # API metadata may not be available yet - if song_data is None: - return - - addon = xbmcaddon.Addon() - slideshow = addon.getSetting('slideshow') - if slideshow == 'rp': - slides = song_data.get('slide_urls') - delay = addon.getSettingInt('slide_duration') - self.slideshow.set_slides(slides, delay) - fanart = self.slideshow.next_slide() - else: - self.slideshow.set_slides(None) - fanart = None - song_key = build_key((song_data['artist'], song_data['title'])) - self.song = Song(song_key, song_data, fanart, start_time) - log(f'Song: {self.song}') - self.update_player() - - def onAVStarted(self): - if self.isPlaying() and self.getPlayingFile() in STREAM_INFO: - url = self.getPlayingFile() - info = STREAM_INFO[url] - # Kodi switches to fullscreen for FLAC, but not AAC - if url == info['url_aac']: - xbmc.executebuiltin('Action(FullScreen)') - else: - self.reset() - - def onPlayBackEnded(self): - if self.stream_url: - self.restart_time = time.time() - else: - self.reset() - - def onPlayBackError(self): - self.reset() - - def onPlayBackStarted(self): - if self.isPlaying() and self.getPlayingFile() in STREAM_INFO: - url = self.getPlayingFile() - self.stream_url = url - self.restart_time = 0 - info = STREAM_INFO[url] - self.now_playing.set_channel(info['channel']) - else: - self.reset() - - def onPlayBackStopped(self): - self.reset() - - -class Slideshow(): - """Provides timed slide URLs.""" - - def __init__(self): - self.set_slides(None) - - def set_slides(self, slides, delay=10): - """Set slides and delay, or None.""" - if slides: - self.slides = slides - self.delay = delay - self.index = 0 - self.time = 0 - else: - self.slides = None - - def next_slide(self): - """Return the next slide URL, or None.""" - result = None - now = time.time() - if self.slides and self.time + self.delay < now: - result = self.slides[self.index] - self.index = (self.index + 1) % len(self.slides) - self.time = now - return result - - -def log(message, level=None): - """Write to the Kodi log.""" - if level is not None: - xbmc.log(f'rp_service: {message}', level) - elif DEVELOPMENT: - xbmc.log(f'rp_service: {message}', xbmc.LOGINFO) - else: - xbmc.log(f'rp_service: {message}', xbmc.LOGDEBUG) - - -def run_service(): - log('Service started.') - player = Player() - monitor = xbmc.Monitor() - while not monitor.abortRequested(): - if monitor.waitForAbort(0.1): - break - try: - player.update() - except Exception as e: - if DEVELOPMENT: - log(traceback.format_exc(), xbmc.LOGERROR) - else: - log(repr(e), xbmc.LOGERROR) - log('Service exiting.') diff --git a/script.radioparadise/resources/settings.xml b/script.radioparadise/resources/settings.xml deleted file mode 100644 index 8fc14729f..000000000 --- a/script.radioparadise/resources/settings.xml +++ /dev/null @@ -1,60 +0,0 @@ - - -
- - - - 0 - aac - - - - - - - - - - 0 - -1 - - - - - - - - - - - - - - - 0 - rp - - - - - - - - - - 0 - 10 - - 10 - 1 - 60 - - - - disabled - - - - -
-
diff --git a/script.radioparadise/resources/skins/Default/720p/script-radioparadise.xml b/script.radioparadise/resources/skins/Default/720p/script-radioparadise.xml deleted file mode 100644 index 0da40d58a..000000000 --- a/script.radioparadise/resources/skins/Default/720p/script-radioparadise.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - 100 - - - 50% - 50% - 180 - 180 - keep - 10 - logo.png - - - 50% - 50% - 180 - 120 - list - - - 10 - - ffffffff - - - - - 0 - menu-focus.png - - - 10 - - ff000000 - - - - - diff --git a/script.radioparadise/resources/skins/Default/media/logo.png b/script.radioparadise/resources/skins/Default/media/logo.png deleted file mode 100644 index 507cb1b52..000000000 Binary files a/script.radioparadise/resources/skins/Default/media/logo.png and /dev/null differ diff --git a/script.radioparadise/resources/skins/Default/media/menu-focus.png b/script.radioparadise/resources/skins/Default/media/menu-focus.png deleted file mode 100644 index 8ef3131b3..000000000 Binary files a/script.radioparadise/resources/skins/Default/media/menu-focus.png and /dev/null differ diff --git a/script.radioparadise/script.py b/script.radioparadise/script.py deleted file mode 100644 index 71f33fe5d..000000000 --- a/script.radioparadise/script.py +++ /dev/null @@ -1,5 +0,0 @@ -from resources.lib.script import run_script - - -if __name__ == '__main__': - run_script() diff --git a/script.radioparadise/service.py b/script.radioparadise/service.py deleted file mode 100644 index 4e19578d9..000000000 --- a/script.radioparadise/service.py +++ /dev/null @@ -1,5 +0,0 @@ -from resources.lib.service import run_service - - -if __name__ == '__main__': - run_service() diff --git a/script.service.hyperion-control/addon.xml b/script.service.hyperion-control/addon.xml deleted file mode 100644 index 71f373222..000000000 --- a/script.service.hyperion-control/addon.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - Control Hyperion Ambilight - Hyperion Ambilight Steuerung - Enable and disable components (like capture) of Hyperion automatically based on playing/screensaver state of Kodi[CR]-Supports auto detection of Hyperion Servers[CR]-Token authentication - Aktiviere und deaktiviere automatisch Hyperion Komponenten basierend auf dem aktuellen Kodi Status[CR]-Unterstützt Hyperion Server suche[CR]-Token Autorisierung - all - MIT - https://hyperion-project.org/forum - https://hyperion-project.org/forum - https://github.com/hyperion-project/hyperion.control - - 20.0.1 - - Removed redundant debug setting - - 20.0.0 - - Kodi 20 (Nexus) support - - Multi instance handling - - New language: Hungarian - - 19.0.2 - - New languages: Español, Français, Polski - - 19.0.1 - - Kodi 19 support - - minor cleanups and corrections - - 1.0.1 - - Fixed a crash with 3d mode @b-jesch - - 1.0.0 - - Added support for server search - - Added support token authentication - - Fixed issue where kodi api does not properly announce video playing states - - - resources/icon.png - resources/fanart.png - resources/screenshot-01.png - - - diff --git a/script.service.hyperion-control/resources/__init__.py b/script.service.hyperion-control/resources/__init__.py deleted file mode 100644 index b80578ea0..000000000 --- a/script.service.hyperion-control/resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Hyperion control addon resources.""" diff --git a/script.service.hyperion-control/resources/fanart.png b/script.service.hyperion-control/resources/fanart.png deleted file mode 100644 index 3944f0514..000000000 Binary files a/script.service.hyperion-control/resources/fanart.png and /dev/null differ diff --git a/script.service.hyperion-control/resources/icon.png b/script.service.hyperion-control/resources/icon.png deleted file mode 100644 index c68c40a30..000000000 Binary files a/script.service.hyperion-control/resources/icon.png and /dev/null differ diff --git a/script.service.hyperion-control/resources/language/resource.language.de_de/strings.po b/script.service.hyperion-control/resources/language/resource.language.de_de/strings.po deleted file mode 100644 index d9e30ed2a..000000000 --- a/script.service.hyperion-control/resources/language/resource.language.de_de/strings.po +++ /dev/null @@ -1,194 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" -"Project-Id-Version: Hyperion Control\n" -"Language: de\n" - -#: -msgctxt "#32000" -msgid "General" -msgstr "Allgemein" - -#: -msgctxt "#32001" -msgid "Hyperion IP" -msgstr "Hyperion IP" - -#: -msgctxt "#32002" -msgid "Hyperion Webserver Port" -msgstr "Hyperion Webserver Port" - -#: -msgctxt "#32003" -msgid "Switch automatically between 2D/3D" -msgstr "Automatisch auf 2D/3D umstellen" - -#: -msgctxt "#32004" -msgid "Enable Hyperion on startup" -msgstr "Aktiviere Hyperion beim Starten" - -#: -msgctxt "#32005" -msgid "Disable Hyperion on shutdown" -msgstr "Deaktiviere Hyperion beim Beenden" - -#: -msgctxt "#32007" -msgid "Show changelog after update" -msgstr "Zeige Änderungen nach Update" - -#: -msgctxt "#32008" -msgid "Authorization Token" -msgstr "Autorisierungs-Token" - -#: -msgctxt "#32025" -msgid "Component" -msgstr "Komponente" - -#: -msgctxt "#32026" -msgid "Enable the chosen component in these situations else disable it." -msgstr "Aktiviere die Komponente in folgenden Situationen, sonst wird sie deaktiviert." - -#: -msgctxt "#32027" -msgid "Hyperion component" -msgstr "Hyperion Komponente" - -#: -msgctxt "#32028" -msgid "Enable during video playback" -msgstr "Aktiviere bei Videos" - -#: -msgctxt "#32029" -msgid "Enable during music playback" -msgstr "Aktiviere bei Musik" - -#: -msgctxt "#32030" -msgid "Enable during player pause" -msgstr "Aktiviere bei pausiertem Player" - -#: -msgctxt "#32031" -msgid "Enable in Kodi menu" -msgstr "Aktiviere im Kodi Menü" - -#: -msgctxt "#32032" -msgid "Enable during active screensaver" -msgstr "Aktiviere bei Bildschirmschoner" - -#: -msgctxt "#32041" -msgid "USB Capture" -msgstr "USB Aufnahme" - -#: -msgctxt "#32042" -msgid "LED Hardware" -msgstr "LED Hardware" - -#: -msgctxt "#32043" -msgid "Smoothing" -msgstr "Glättung" - -#: -msgctxt "#32045" -msgid "Forwarder" -msgstr "Weiterleitung" - -#: -msgctxt "#32101" -msgid "Would you like to search for a Hyperion Server and adjust settings?" -msgstr "Hyperion Server suchen und Einstellungen anpassen?" - -#: -msgctxt "#32102" -msgid "Select a Hyperion Server, we found more than one" -msgstr "Wähle einen Hyperion Server, es wurden mehrere gefunden" - -#: -msgctxt "#32104" -msgid "We are sorry, no Hyperion Server has been found. You need to configure IP address and port by hand" -msgstr "Leider wurde kein Hyperion Server gefunden, du musst IP-Adresse und Port selbst eintragen" - -#: -msgctxt "#32105" -msgid "The Authorization Token isn't valid" -msgstr "Das Autorisierungs-Token ist nicht gültig" - -#: -msgctxt "#32150" -msgid "Execute" -msgstr "Ausführen" - -#: -msgctxt "#32151" -msgid "Execute a specific task you might need again.[CR]Select a task from the tasklist and press OK to close the settings dialog." -msgstr "Führe eine spezifische Aufgabe nochmals aus.[CR]Wähle dazu aus der Liste eine Aufgabe und drücke den OK Button." - -#: -msgctxt "#32152" -msgid "The addon will handle your request immediately.[CR]Be aware that the addon should be already enabled" -msgstr "Die Einstellungen schließen sich.[CR]Das Addon wird sofort mit der Ausführung beginnen, sofern es aktiviert ist" - -#: -msgctxt "#32153" -msgid "Tasklist" -msgstr "Aufgabenliste" - -#: -msgctxt "#32154" -msgid "No Task" -msgstr "keine Aufgabe" - -#: -msgctxt "#32155" -msgid "Search: Hyperion Server" -msgstr "Suchen: Hyperion Server" - -#: -msgctxt "#32044" -msgid "Blackbar detector" -msgstr "Schwarze Balken Erkennung" - -#: -msgctxt "#32040" -msgid "Screen Capture" -msgstr "Bildschirm Aufnahme" - -#: -msgctxt "#32100" -msgid "Welcome to Hyperion Control!" -msgstr "Willkommen zu Hyperion Control!" - -#: -msgctxt "#32103" -msgid "We found the following Hyperion Server for usage:" -msgstr "Es wurde folgender Hyperion Server gefunden:" - -#: -msgctxt "#32046" -msgid "Boblight Server" -msgstr "Boblight Server" - -#: -msgctxt "#32047" -msgid "Hyperion" -msgstr "Hyperion" - -#: -msgctxt "#32048" -msgid "Audio Capture" -msgstr "Audio Aufnahme" - diff --git a/script.service.hyperion-control/resources/language/resource.language.en_gb/strings.po b/script.service.hyperion-control/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index 8ce63453e..000000000 --- a/script.service.hyperion-control/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,194 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" -"Project-Id-Version: Hyperion Control\n" -"Language: en\n" - -#: -msgctxt "#32000" -msgid "General" -msgstr "General" - -#: -msgctxt "#32001" -msgid "Hyperion IP" -msgstr "Hyperion IP" - -#: -msgctxt "#32002" -msgid "Hyperion Webserver Port" -msgstr "Hyperion Webserver Port" - -#: -msgctxt "#32003" -msgid "Switch automatically between 2D/3D" -msgstr "Switch automatically between 2D/3D" - -#: -msgctxt "#32004" -msgid "Enable Hyperion on startup" -msgstr "Enable Hyperion on startup" - -#: -msgctxt "#32005" -msgid "Disable Hyperion on shutdown" -msgstr "Disable Hyperion on shutdown" - -#: -msgctxt "#32007" -msgid "Show changelog after update" -msgstr "Show changelog after update" - -#: -msgctxt "#32008" -msgid "Authorization Token" -msgstr "Authorization Token" - -#: -msgctxt "#32025" -msgid "Component" -msgstr "Component" - -#: -msgctxt "#32026" -msgid "Enable the chosen component in these situations else disable it." -msgstr "Enable the chosen component in these situations else disable it." - -#: -msgctxt "#32027" -msgid "Hyperion component" -msgstr "Hyperion component" - -#: -msgctxt "#32028" -msgid "Enable during video playback" -msgstr "Enable during video playback" - -#: -msgctxt "#32029" -msgid "Enable during music playback" -msgstr "Enable during music playback" - -#: -msgctxt "#32030" -msgid "Enable during player pause" -msgstr "Enable during player pause" - -#: -msgctxt "#32031" -msgid "Enable in Kodi menu" -msgstr "Enable in Kodi menu" - -#: -msgctxt "#32032" -msgid "Enable during active screensaver" -msgstr "Enable during active screensaver" - -#: -msgctxt "#32041" -msgid "USB Capture" -msgstr "USB Capture" - -#: -msgctxt "#32042" -msgid "LED Hardware" -msgstr "LED Hardware" - -#: -msgctxt "#32043" -msgid "Smoothing" -msgstr "Smoothing" - -#: -msgctxt "#32045" -msgid "Forwarder" -msgstr "Forwarder" - -#: -msgctxt "#32101" -msgid "Would you like to search for a Hyperion Server and adjust settings?" -msgstr "Would you like to search for a Hyperion Server and adjust settings?" - -#: -msgctxt "#32102" -msgid "Select a Hyperion Server, we found more than one" -msgstr "Select a Hyperion Server, we found more than one" - -#: -msgctxt "#32104" -msgid "We are sorry, no Hyperion Server has been found. You need to configure IP address and port by hand" -msgstr "We are sorry, no Hyperion Server has been found. You need to configure IP address and port by hand" - -#: -msgctxt "#32105" -msgid "The Authorization Token isn't valid" -msgstr "The Authorization Token isn't valid" - -#: -msgctxt "#32150" -msgid "Execute" -msgstr "Execute" - -#: -msgctxt "#32151" -msgid "Execute a specific task you might need again.[CR]Select a task from the tasklist and press OK to close the settings dialog." -msgstr "Execute a specific task you might need again.[CR]Select a task from the tasklist and press OK to close the settings dialog." - -#: -msgctxt "#32152" -msgid "The addon will handle your request immediately.[CR]Be aware that the addon should be already enabled" -msgstr "The addon will handle your request immediately.[CR]Be aware that the addon should be already enabled" - -#: -msgctxt "#32153" -msgid "Tasklist" -msgstr "Tasklist" - -#: -msgctxt "#32154" -msgid "No Task" -msgstr "No Task" - -#: -msgctxt "#32155" -msgid "Search: Hyperion Server" -msgstr "Search: Hyperion Server" - -#: -msgctxt "#32044" -msgid "Blackbar detector" -msgstr "Blackbar detector" - -#: -msgctxt "#32040" -msgid "Screen Capture" -msgstr "Screen Capture" - -#: -msgctxt "#32100" -msgid "Welcome to Hyperion Control!" -msgstr "Welcome to Hyperion Control!" - -#: -msgctxt "#32103" -msgid "We found the following Hyperion Server for usage:" -msgstr "We found the following Hyperion Server for usage:" - -#: -msgctxt "#32046" -msgid "Boblight Server" -msgstr "Boblight Server" - -#: -msgctxt "#32047" -msgid "Hyperion" -msgstr "Hyperion" - -#: -msgctxt "#32048" -msgid "Audio Capture" -msgstr "Audio Capture" - diff --git a/script.service.hyperion-control/resources/language/resource.language.es_es/strings.po b/script.service.hyperion-control/resources/language/resource.language.es_es/strings.po deleted file mode 100644 index fcc547953..000000000 --- a/script.service.hyperion-control/resources/language/resource.language.es_es/strings.po +++ /dev/null @@ -1,194 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" -"Project-Id-Version: Hyperion Control\n" -"Language: es\n" - -#: -msgctxt "#32000" -msgid "General" -msgstr "General" - -#: -msgctxt "#32001" -msgid "Hyperion IP" -msgstr "IP de Hyperion" - -#: -msgctxt "#32002" -msgid "Hyperion Webserver Port" -msgstr "Puerto del servidor web de Hyperion" - -#: -msgctxt "#32003" -msgid "Switch automatically between 2D/3D" -msgstr "Cambio automático entre 2D/3D" - -#: -msgctxt "#32004" -msgid "Enable Hyperion on startup" -msgstr "Habilitar Hyperion al inicio" - -#: -msgctxt "#32005" -msgid "Disable Hyperion on shutdown" -msgstr "Deshabilitar Hyperion al apagar" - -#: -msgctxt "#32007" -msgid "Show changelog after update" -msgstr "Mostrar el registro de cambios tras la actualización" - -#: -msgctxt "#32008" -msgid "Authorization Token" -msgstr "Token de autorización" - -#: -msgctxt "#32025" -msgid "Component" -msgstr "Componente" - -#: -msgctxt "#32026" -msgid "Enable the chosen component in these situations else disable it." -msgstr "Habilitar el componente elegido en estas situaciones o deshabilitarlo." - -#: -msgctxt "#32027" -msgid "Hyperion component" -msgstr "Componente Hyperion" - -#: -msgctxt "#32028" -msgid "Enable during video playback" -msgstr "Activar durante la reproducción de vídeo" - -#: -msgctxt "#32029" -msgid "Enable during music playback" -msgstr "Activar durante la reproducción de música" - -#: -msgctxt "#32030" -msgid "Enable during player pause" -msgstr "Habilitar durante la pausa del reproductor" - -#: -msgctxt "#32031" -msgid "Enable in Kodi menu" -msgstr "Habilitar en el menú de Kodi" - -#: -msgctxt "#32032" -msgid "Enable during active screensaver" -msgstr "Habilitar durante el salvapantallas activo" - -#: -msgctxt "#32041" -msgid "USB Capture" -msgstr "Captura USB" - -#: -msgctxt "#32042" -msgid "LED Hardware" -msgstr "Hardware LED" - -#: -msgctxt "#32043" -msgid "Smoothing" -msgstr "Suavizado" - -#: -msgctxt "#32045" -msgid "Forwarder" -msgstr "Transmisor" - -#: -msgctxt "#32101" -msgid "Would you like to search for a Hyperion Server and adjust settings?" -msgstr "¿Desea buscar un Servidor Hyperion y ajustar la configuración?" - -#: -msgctxt "#32102" -msgid "Select a Hyperion Server, we found more than one" -msgstr "Seleccione un servidor Hyperion, encontramos más de uno" - -#: -msgctxt "#32104" -msgid "We are sorry, no Hyperion Server has been found. You need to configure IP address and port by hand" -msgstr "Lo sentimos, no se ha encontrado ningún Hyperion Server. Debe configurar la dirección IP y el puerto a mano" - -#: -msgctxt "#32105" -msgid "The Authorization Token isn't valid" -msgstr "Lo sentimos, no se ha encontrado ningún Servidor Hyperion. Es necesario configurar la dirección IP y el puerto a mano" - -#: -msgctxt "#32150" -msgid "Execute" -msgstr "Ejecutar" - -#: -msgctxt "#32151" -msgid "Execute a specific task you might need again.[CR]Select a task from the tasklist and press OK to close the settings dialog." -msgstr "Ejecutar una tarea específica que se pueda necesitar de nuevo.[CR]Seleccionar una tarea de la lista de tareas y pulsar OK para cerrar el diálogo de configuración." - -#: -msgctxt "#32152" -msgid "The addon will handle your request immediately.[CR]Be aware that the addon should be already enabled" -msgstr "El complemento se encargará de la solicitud inmediatamente.[CR]Ten en cuenta que el complemento debe estar ya activado" - -#: -msgctxt "#32153" -msgid "Tasklist" -msgstr "Lista de tareas" - -#: -msgctxt "#32154" -msgid "No Task" -msgstr "Sin Tareas" - -#: -msgctxt "#32155" -msgid "Search: Hyperion Server" -msgstr "Buscar: Servidor Hyperion" - -#: -msgctxt "#32044" -msgid "Blackbar detector" -msgstr "Detección de bordes negros" - -#: -msgctxt "#32040" -msgid "Screen Capture" -msgstr "Captura de Plataforma" - -#: -msgctxt "#32100" -msgid "Welcome to Hyperion Control!" -msgstr "Bienvenidol Control de Hyperion!" - -#: -msgctxt "#32103" -msgid "We found the following Hyperion Server for usage:" -msgstr "Encontramos los siguientes servidores Hyperion para su uso:" - -#: -msgctxt "#32046" -msgid "Boblight Server" -msgstr "Servidor Boblight" - -#: -msgctxt "#32047" -msgid "Hyperion" -msgstr "Hyperion" - -#: -msgctxt "#32048" -msgid "Audio Capture" -msgstr "Captura del Audio" - diff --git a/script.service.hyperion-control/resources/language/resource.language.fr_fr/strings.po b/script.service.hyperion-control/resources/language/resource.language.fr_fr/strings.po deleted file mode 100644 index 72f022042..000000000 --- a/script.service.hyperion-control/resources/language/resource.language.fr_fr/strings.po +++ /dev/null @@ -1,196 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" -"Project-Id-Version: Hyperion Control\n" -"Language: fr\n" - -#: -msgctxt "#32000" -msgid "General" -msgstr "Général" - -#: -msgctxt "#32001" -msgid "Hyperion IP" -msgstr "Adresse IP Hyperion" - -#: -msgctxt "#32002" -msgid "Hyperion Webserver Port" -msgstr "Port Webserver Hyperion" - -#: -msgctxt "#32003" -msgid "Switch automatically between 2D/3D" -msgstr "Basculer automatiquement entre 2D/3D" - -#: -msgctxt "#32004" -msgid "Enable Hyperion on startup" -msgstr "Activer Hyperion au démarrage" - -#: -msgctxt "#32005" -msgid "Disable Hyperion on shutdown" -msgstr "Désactiver Hyperion à l'extinction" - -#: -msgctxt "#32007" -msgid "Show changelog after update" -msgstr "Afficher le changelog après la mise à jour" - -#: -msgctxt "#32008" -msgid "Authorization Token" -msgstr "Token d'autorisation" - -#: -msgctxt "#32025" -msgid "Component" -msgstr "Composant" - -#: -msgctxt "#32026" -msgid "Enable the chosen component in these situations else disable it." -msgstr "Activer le composant pour ces situations sinon le désactiver." - -#: -msgctxt "#32027" -msgid "Hyperion component" -msgstr "Composant Hyperion" - -#: -msgctxt "#32028" -msgid "Enable during video playback" -msgstr "Activer durant la lecture vidéo" - -#: -msgctxt "#32029" -msgid "Enable during music playback" -msgstr "Activer durant la lecture audio" - -#: -msgctxt "#32030" -msgid "Enable during player pause" -msgstr "Activer pendant la pause du lecteur" - -#: -msgctxt "#32031" -msgid "Enable in Kodi menu" -msgstr "Activer dans le menu de Kodi" - -#: -msgctxt "#32032" -msgid "Enable during active screensaver" -msgstr "Activer quand l'économiseur d'écran est actif" - -#: -msgctxt "#32041" -msgid "USB Capture" -msgstr "Capture USB" - -#: -msgctxt "#32042" -msgid "LED Hardware" -msgstr "LEDs" - -#: -msgctxt "#32043" -msgid "Smoothing" -msgstr "Lissage" - -#: -msgctxt "#32045" -msgid "Forwarder" -msgstr "Forwarder" - -#: -msgctxt "#32101" -msgid "Would you like to search for a Hyperion Server and adjust settings?" -msgstr "Voulez vous chercher un serveur Hyperion et mettre à jour les paramètres ?" - -#: -msgctxt "#32102" -msgid "Select a Hyperion Server, we found more than one" -msgstr "Plusieurs serveurs Hyperion ont été trouvés, veuillez en sélectionner un" - -#: -msgctxt "#32104" -msgid "We are sorry, no Hyperion Server has been found. You need to configure IP address and port by hand" -msgstr "Désolé, aucun serveur Hyperion trouvé. Configurez l'adresse IP et le port manuellement." - -#: -msgctxt "#32105" -msgid "The Authorization Token isn't valid" -msgstr "Le token d'autorisation n'est pas valide" - -#: -msgctxt "#32150" -msgid "Execute" -msgstr "Exécuter" - -#: -msgctxt "#32151" -msgid "Execute a specific task you might need again.[CR]Select a task from the tasklist and press OK to close the settings dialog." -msgstr "Exécuter une tâche spécifique dont vous pourriez avoir besoin à nouveau.\n" -"Choisir une tâche dans la liste et appuyer sur OK pour fermer la boite de dialogue." - -#: -msgctxt "#32152" -msgid "The addon will handle your request immediately.[CR]Be aware that the addon should be already enabled" -msgstr "L'addon va exécuter votre requête immédiatement.\n" -"Attention : l'addon doit déjà être actif" - -#: -msgctxt "#32153" -msgid "Tasklist" -msgstr "Liste de tâches" - -#: -msgctxt "#32154" -msgid "No Task" -msgstr "Pas de tâche" - -#: -msgctxt "#32155" -msgid "Search: Hyperion Server" -msgstr "Recherche : serveur Hyperion" - -#: -msgctxt "#32044" -msgid "Blackbar detector" -msgstr "Détecteur de bandes noires" - -#: -msgctxt "#32040" -msgid "Screen Capture" -msgstr "Capture interne" - -#: -msgctxt "#32100" -msgid "Welcome to Hyperion Control!" -msgstr "Bienvenue dans l'addon Hyperion!" - -#: -msgctxt "#32103" -msgid "We found the following Hyperion Server for usage:" -msgstr "Le serveur Hyperion suivant a été trouvé pour utilisation:" - -#: -msgctxt "#32046" -msgid "Boblight Server" -msgstr "Serveur Boblight" - -#: -msgctxt "#32047" -msgid "Hyperion" -msgstr "Hyperion" - -#: -msgctxt "#32048" -msgid "Audio Capture" -msgstr "Capture Audio" - diff --git a/script.service.hyperion-control/resources/language/resource.language.hu_hu/strings.po b/script.service.hyperion-control/resources/language/resource.language.hu_hu/strings.po deleted file mode 100644 index 49e70f80d..000000000 --- a/script.service.hyperion-control/resources/language/resource.language.hu_hu/strings.po +++ /dev/null @@ -1,194 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" -"Project-Id-Version: Hyperion Control\n" -"Language: hu\n" - -#: -msgctxt "#32000" -msgid "General" -msgstr "Általános" - -#: -msgctxt "#32001" -msgid "Hyperion IP" -msgstr "Hyperion IP" - -#: -msgctxt "#32002" -msgid "Hyperion Webserver Port" -msgstr "Hyperion Webserver Port" - -#: -msgctxt "#32003" -msgid "Switch automatically between 2D/3D" -msgstr "Automatikus átkapcsolás 2D/3D" - -#: -msgctxt "#32004" -msgid "Enable Hyperion on startup" -msgstr "Hyperion automatikus indítás" - -#: -msgctxt "#32005" -msgid "Disable Hyperion on shutdown" -msgstr "Hyperion kikapcsolása leállításkor" - -#: -msgctxt "#32007" -msgid "Show changelog after update" -msgstr "Változásnapló megjelenítése frissítés után" - -#: -msgctxt "#32008" -msgid "Authorization Token" -msgstr "Hozzáférési Token" - -#: -msgctxt "#32025" -msgid "Component" -msgstr "Komponens" - -#: -msgctxt "#32026" -msgid "Enable the chosen component in these situations else disable it." -msgstr "Engedélyezze a kiválasztott összetevőt, ellenkező esetben tiltsa le." - -#: -msgctxt "#32027" -msgid "Hyperion component" -msgstr "Hyperion komponens" - -#: -msgctxt "#32028" -msgid "Enable during video playback" -msgstr "Engedélyezés videólejátszás közben" - -#: -msgctxt "#32029" -msgid "Enable during music playback" -msgstr "Engedélyezés zenelejátszás közben" - -#: -msgctxt "#32030" -msgid "Enable during player pause" -msgstr "Engedélyezés lejátszási szünetben" - -#: -msgctxt "#32031" -msgid "Enable in Kodi menu" -msgstr "Engedélyezés a Kodi menüben" - -#: -msgctxt "#32032" -msgid "Enable during active screensaver" -msgstr "Engedélyezés aktív képernyővédő alatt" - -#: -msgctxt "#32041" -msgid "USB Capture" -msgstr "USB Capture" - -#: -msgctxt "#32042" -msgid "LED Hardware" -msgstr "LED Hardver" - -#: -msgctxt "#32043" -msgid "Smoothing" -msgstr "Símítás" - -#: -msgctxt "#32045" -msgid "Forwarder" -msgstr "Továbbító" - -#: -msgctxt "#32101" -msgid "Would you like to search for a Hyperion Server and adjust settings?" -msgstr "Szeretne keresni egy Hyperion szervert és módosítani a beállításokat?" - -#: -msgctxt "#32102" -msgid "Select a Hyperion Server, we found more than one" -msgstr "Válasszon egy Hyperion kiszolgálót, többet is találtunk" - -#: -msgctxt "#32104" -msgid "We are sorry, no Hyperion Server has been found. You need to configure IP address and port by hand" -msgstr "Sajnáljuk, nem található Hyperion Server. Az IP-címet és a portot kézzel kell konfigurálnia!" - -#: -msgctxt "#32105" -msgid "The Authorization Token isn't valid" -msgstr "Az engedélyezési token nem érvényes" - -#: -msgctxt "#32150" -msgid "Execute" -msgstr "Végrehajtás" - -#: -msgctxt "#32151" -msgid "Execute a specific task you might need again.[CR]Select a task from the tasklist and press OK to close the settings dialog." -msgstr "Hajtsa végre újra az adott feladatot, amelyre szüksége lehet.[CR]Válasszon ki egy feladatot a feladatlistából, és nyomja meg az OK gombot a beállítások párbeszédpanel bezárásához." - -#: -msgctxt "#32152" -msgid "The addon will handle your request immediately.[CR]Be aware that the addon should be already enabled" -msgstr "A bővítmény azonnal kezeli a kérést.[CR]Ne feledje, hogy a kiegészítőnek már engedélyezve kell lennie" - -#: -msgctxt "#32153" -msgid "Tasklist" -msgstr "Feladat lista" - -#: -msgctxt "#32154" -msgid "No Task" -msgstr "Nincs feladat" - -#: -msgctxt "#32155" -msgid "Search: Hyperion Server" -msgstr "Keresés: Hyperion Server" - -#: -msgctxt "#32044" -msgid "Blackbar detector" -msgstr "Feketesáv detector" - -#: -msgctxt "#32040" -msgid "Screen Capture" -msgstr "Screen Capture" - -#: -msgctxt "#32100" -msgid "Welcome to Hyperion Control!" -msgstr "Üdvözli a Hyperion Control!" - -#: -msgctxt "#32103" -msgid "We found the following Hyperion Server for usage:" -msgstr "A következő Hyperion szervert találtuk használatra:" - -#: -msgctxt "#32046" -msgid "Boblight Server" -msgstr "Boblight Server" - -#: -msgctxt "#32047" -msgid "Hyperion" -msgstr "Hyperion" - -#: -msgctxt "#32048" -msgid "Audio Capture" -msgstr "Hangrögzítés" - diff --git a/script.service.hyperion-control/resources/language/resource.language.pl_pl/strings.po b/script.service.hyperion-control/resources/language/resource.language.pl_pl/strings.po deleted file mode 100644 index bba29cecb..000000000 --- a/script.service.hyperion-control/resources/language/resource.language.pl_pl/strings.po +++ /dev/null @@ -1,194 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" -"Project-Id-Version: Hyperion Control\n" -"Language: pl\n" - -#: -msgctxt "#32000" -msgid "General" -msgstr "Ogólne" - -#: -msgctxt "#32001" -msgid "Hyperion IP" -msgstr "Hyperion IP" - -#: -msgctxt "#32002" -msgid "Hyperion Webserver Port" -msgstr "Port Hyperion Webserver" - -#: -msgctxt "#32003" -msgid "Switch automatically between 2D/3D" -msgstr "Zmieniaj automatycznie 2D/3D" - -#: -msgctxt "#32004" -msgid "Enable Hyperion on startup" -msgstr "Uruchom Hyperion wraz ze startem systemu" - -#: -msgctxt "#32005" -msgid "Disable Hyperion on shutdown" -msgstr "Wyłącz Hyperion przy zamknięciu systemu" - -#: -msgctxt "#32007" -msgid "Show changelog after update" -msgstr "Pokaż dziennik zmian po aktualizacji" - -#: -msgctxt "#32008" -msgid "Authorization Token" -msgstr "Token autoryzacyjny" - -#: -msgctxt "#32025" -msgid "Component" -msgstr "Komponent" - -#: -msgctxt "#32026" -msgid "Enable the chosen component in these situations else disable it." -msgstr "Włącz wybrany komponent w wybranych sytuacjach - w przeciwnym wypadku wyłącz komponent." - -#: -msgctxt "#32027" -msgid "Hyperion component" -msgstr "Komponent Hyperion" - -#: -msgctxt "#32028" -msgid "Enable during video playback" -msgstr "Włącz podczas odtwarzania wideo" - -#: -msgctxt "#32029" -msgid "Enable during music playback" -msgstr "Włącz podczas odtwarzania muzyki" - -#: -msgctxt "#32030" -msgid "Enable during player pause" -msgstr "Włącz podczas zapauzowania odtwarzacza" - -#: -msgctxt "#32031" -msgid "Enable in Kodi menu" -msgstr "Włącz w menu Kodi" - -#: -msgctxt "#32032" -msgid "Enable during active screensaver" -msgstr "Włącz podczas aktywnego wygaszacza ekranu" - -#: -msgctxt "#32041" -msgid "USB Capture" -msgstr "Przechwytywanie USB" - -#: -msgctxt "#32042" -msgid "LED Hardware" -msgstr "Sprzęt LED" - -#: -msgctxt "#32043" -msgid "Smoothing" -msgstr "Wygładzanie" - -#: -msgctxt "#32045" -msgid "Forwarder" -msgstr "Przekierowywacz" - -#: -msgctxt "#32101" -msgid "Would you like to search for a Hyperion Server and adjust settings?" -msgstr "Czy chcesz wyszukać serwer Hyperion i dostosować ustawienia?" - -#: -msgctxt "#32102" -msgid "Select a Hyperion Server, we found more than one" -msgstr "Wybierz serwer Hyperion, znaleziono więcej niż jeden" - -#: -msgctxt "#32104" -msgid "We are sorry, no Hyperion Server has been found. You need to configure IP address and port by hand" -msgstr "Niestety, nie znaleziono serwera Hyperion. Skonfiguruj adres IP oraz port manualnie" - -#: -msgctxt "#32105" -msgid "The Authorization Token isn't valid" -msgstr "Nieprawidłowy token autoryzacyjny" - -#: -msgctxt "#32150" -msgid "Execute" -msgstr "Wykonaj" - -#: -msgctxt "#32151" -msgid "Execute a specific task you might need again.[CR]Select a task from the tasklist and press OK to close the settings dialog." -msgstr "Wykonaj zadanie ponownie. Wybierz zadanie z listy zadań i wciśnij OK aby zamknąć okno ustawień." - -#: -msgctxt "#32152" -msgid "The addon will handle your request immediately.[CR]Be aware that the addon should be already enabled" -msgstr "Twoje zadanie zostanie wykonane natychmiastowo. Miej na uwadze, że addon powinien być wcześniej włączony" - -#: -msgctxt "#32153" -msgid "Tasklist" -msgstr "Lista zadań" - -#: -msgctxt "#32154" -msgid "No Task" -msgstr "Brak zadania" - -#: -msgctxt "#32155" -msgid "Search: Hyperion Server" -msgstr "Wyszukaj: Serwer Hyperion" - -#: -msgctxt "#32044" -msgid "Blackbar detector" -msgstr "Detekcja czarnych ramek" - -#: -msgctxt "#32040" -msgid "Screen Capture" -msgstr "Przechwytywanie platformy" - -#: -msgctxt "#32100" -msgid "Welcome to Hyperion Control!" -msgstr "Witaj w Hyperion Control!" - -#: -msgctxt "#32103" -msgid "We found the following Hyperion Server for usage:" -msgstr "Znaleziono następujący serwer Hyperion:" - -#: -msgctxt "#32046" -msgid "Boblight Server" -msgstr "Server Boblight" - -#: -msgctxt "#32047" -msgid "Hyperion" -msgstr "Hyperion" - -#: -msgctxt "#32048" -msgid "Audio Capture" -msgstr "Przechwytywanie Dźwięku" - diff --git a/script.service.hyperion-control/resources/lib/__init__.py b/script.service.hyperion-control/resources/lib/__init__.py deleted file mode 100644 index ba7fe2645..000000000 --- a/script.service.hyperion-control/resources/lib/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Hyperion control addon library.""" diff --git a/script.service.hyperion-control/resources/lib/api_client.py b/script.service.hyperion-control/resources/lib/api_client.py deleted file mode 100644 index d211c42fa..000000000 --- a/script.service.hyperion-control/resources/lib/api_client.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Hyperion JSON RPC/HTTP(S) API client.""" -from __future__ import annotations - -import contextlib -import random -import string -from json import JSONDecodeError -from typing import Any - -import requests -from requests.exceptions import ConnectTimeout - -from resources.lib.interfaces import GuiHandler -from resources.lib.interfaces import Logger -from resources.lib.interfaces import SettingsManager - - -class ApiClient: - """Manages the request to the hyperion server.""" - - def __init__( - self, logger: Logger, gui: GuiHandler, settings: SettingsManager - ) -> None: - self._settings = settings - self._logger = logger - self._gui = gui - self._session = requests.Session() - - @property - def headers(self) -> dict[str, str]: - """Request headers.""" - headers = {"Content-type": "application/json"} - if self._settings.auth_token: - headers["Authorization"] = f"token {self._settings.auth_token}" - return headers - - def _send( - self, body: dict[str, Any], timeout: float = 0.5 - ) -> dict[str, Any] | None: - url = self._settings.base_url - logger = self._logger - logger.log(f"Send to: {url} payload: {body}") - with contextlib.suppress(ConnectTimeout, JSONDecodeError): - response = self._session.post( - url, json=body, headers=self.headers, timeout=timeout - ) - json_content = response.json() - if json_content.get("success"): - return json_content.get("info") - if json_content["error"] == "No Authorization": - self._gui.notify_text("Error: No Authorization, API Token required") - logger.error(json_content["error"]) - return None - - def needs_auth(self) -> bool: - """Whether the hyperion server needs API authentication.""" - if res := self._send({"command": "authorize", "subcommand": "tokenRequired"}): - return res["required"] - return False - - def get_token(self) -> str: - """Requests the authentication token.""" - pool = string.ascii_uppercase + string.ascii_lowercase + string.digits - control_code = "".join(random.choice(pool) for _ in range(16)) - message = { - "command": "authorize", - "subcommand": "requestToken", - "comment": "Kodi Hyperion Control", - "id": control_code, - } - return res["token"] if (res := self._send(message, timeout=180)) else "" - - def send_component_state(self, component: str, state: bool) -> None: - """Sends the component state.""" - body = { - "command": "componentstate", - "componentstate": {"component": component, "state": state}, - } - if component == "FORWARDER": - self.switch_to_instance(0) - self._send(body) - else: - self.send_to_all_instances(body) - - def send_video_mode(self, mode: str) -> None: - """Sends the current video mode.""" - self._send({"command": "videoMode", "videoMode": mode}) - - def get_server_info(self) -> dict[str, Any] | None: - """Gets the server info.""" - return self._send({"command": "serverinfo"}) - - def get_instances(self) -> list[dict[str, Any]]: - """Gets the server info.""" - server_info = self.get_server_info() - return server_info["instance"] if server_info else [] - - def switch_to_instance(self, instance_num: int) -> None: - """Switches to the specified instance.""" - self._send( - {"command": "instance", "subcommand": "switchTo", "instance": instance_num} - ) - - def send_to_all_instances(self, body: dict[str, Any]) -> None: - """Sends a command to all instances.""" - for instance in self.get_instances(): - self.switch_to_instance(instance["instance"]) - self._send(body) diff --git a/script.service.hyperion-control/resources/lib/gui.py b/script.service.hyperion-control/resources/lib/gui.py deleted file mode 100644 index 8bb6410ea..000000000 --- a/script.service.hyperion-control/resources/lib/gui.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Kodi GUI handler.""" -from __future__ import annotations - -import xbmcaddon -import xbmcgui - -from resources.lib import ssdp -from resources.lib.interfaces import SettingsManager - - -class GuiHandler: - """Kodi GUI handler.""" - - def __init__( - self, addon: xbmcaddon.Addon, settings_manager: SettingsManager - ) -> None: - self._addon = addon - self._settings = settings_manager - self._dialog = xbmcgui.Dialog() # TODO: DI with embedded getlocalizedstring - self._addon_name = addon.getAddonInfo("name") - self._addon_icon = addon.getAddonInfo("icon") - - def _get_localized_string(self, label_id: int) -> str: - """Returns the localized string of a label ID.""" - return self._addon.getLocalizedString(label_id) - - def notify_label(self, label_id: int) -> None: - """Displays a notification with the localized message.""" - message = self._get_localized_string(label_id) - self.notify_text(message, time=1000, icon=self._addon_icon) - - def notify_text( - self, message: str, time: int = 3000, icon: str = xbmcgui.NOTIFICATION_INFO - ) -> None: - """Displays a notification.""" - self._dialog.notification(self._addon_name, message, icon, time) - - def do_ssdp_discovery(self) -> None: - """Perform the SSDP discovery and lets the user choose the service.""" - servers = ssdp.discover() - - if not servers: - self._dialog.ok("Hyperion Control", self._get_localized_string(32104)) - return - # if there is more than one entry the user should select one - if len(servers) > 1: - selection_idx = self._dialog.select( - self._get_localized_string(32102), build_select_list(servers) - ) - selected_server = servers[selection_idx] if selection_idx > -1 else None - else: - selected_server = servers[0] - self._dialog.ok( - "Hyperion Control", - f'{self._get_localized_string(32103)}[CR]{selected_server["ip"]}:{selected_server["port"]}', - ) - - if selected_server: - self._settings.address = selected_server["ip"] - self._settings.port = selected_server["port"] - - def do_initial_wizard(self) -> None: - """Displays the initial wizard.""" - if self._dialog.yesno( - "Hyperion Control", - f"{self._get_localized_string(32100)}[CR]{self._get_localized_string(32101)}", - ): - self.do_ssdp_discovery() - self._addon.openSettings() - - def do_changelog_display(self) -> None: - """Displays the changelog.""" - if self._settings.current_version != self._addon.getAddonInfo("version"): - self._dialog.textviewer( - "Hyperion Control - Changelog", self._addon.getAddonInfo("changelog") - ) - - -def build_select_list(data: list[dict[str, str | int | None]]) -> list[str]: - return [f"{item['ip']}:{item['port']}" for item in data] diff --git a/script.service.hyperion-control/resources/lib/hyperion.py b/script.service.hyperion-control/resources/lib/hyperion.py deleted file mode 100644 index cba18bd80..000000000 --- a/script.service.hyperion-control/resources/lib/hyperion.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Hyperion Controller.""" -from __future__ import annotations - -from collections.abc import Callable - -from resources.lib.interfaces import ApiClient -from resources.lib.interfaces import GuiHandler -from resources.lib.interfaces import Logger -from resources.lib.interfaces import SettingsManager - - -class Hyperion: - """Main instance class.""" - - def __init__( - self, - settings_manager: SettingsManager, - logger: Logger, - gui_handler: GuiHandler, - api_client: ApiClient, - get_video_mode_function: Callable[[], str], - addon_version: str, - ) -> None: - self._prev_video_mode = "2D" - - self._addon_version = addon_version - self._client = api_client - self._settings = settings_manager - self._logger = logger - self._gui = gui_handler - self._video_mode_fn = get_video_mode_function - - self._kodi_state: str = "menu" - self._prev_comp_state: bool | None = None - self._initialize() - - def _initialize(self) -> None: - # check for changelog display, but not on first run - settings = self._settings - if settings.should_display_changelog: - self._gui.do_changelog_display() - - # check for setup wizard - if settings.first_run: - # be sure to fill in the current version - settings.set_addon_version(self._addon_version) - self._gui.do_initial_wizard() - - if settings.enable_hyperion: - self._client.send_component_state("ALL", True) - settings.set_first_run_done() - - def notify(self, command: str) -> None: - """Process the commands sent by the observables.""" - self._logger.log(f"received command: {command}") - if command == "updateSettings": - self.update_settings() - else: - self._kodi_state = command - self._update_state() - - def update_settings(self) -> None: - """Update the settings.""" - settings = self._settings - settings.read_settings() - - auth_token = settings.auth_token - if auth_token and len(auth_token) != 36: - self._gui.notify_label(32105) - - # Checkout Tasklist for pending tasks - if settings.tasks == 1: - settings.set_tasks(0) - self._gui.do_ssdp_discovery() - - def _update_state(self) -> None: - comp_state = self._get_comp_state() - if self._prev_comp_state != comp_state: - self._client.send_component_state(self._settings.target_comp, comp_state) - self._prev_comp_state = comp_state - - # update stereoscopic mode always, better apis for detection available? - # Bug: race condition, return of jsonapi has wrong gui state - # after onPlayBackStopped after a 3D movie - if self._settings.video_mode_enabled: - new_mode = self._video_mode_fn() - if self._prev_video_mode != new_mode: - self._client.send_video_mode(new_mode) - self._prev_video_mode = new_mode - - def _get_comp_state(self) -> bool: - """Get the desired state of the target component based on kodi state.""" - settings = self._settings - state = self._kodi_state - if state == "screensaver": - return settings.screensaver_enabled - if state == "pause": - return settings.pause_enabled - if state == "playAudio": - return settings.audio_enabled - if state == "playVideo": - return settings.video_enabled - return settings.menu_enabled - - def stop(self) -> None: - """Stops the hyperion control.""" - if self._settings.disable_hyperion: - self._client.send_component_state("ALL", False) - self._logger.log("Hyperion-control stopped") diff --git a/script.service.hyperion-control/resources/lib/interfaces.py b/script.service.hyperion-control/resources/lib/interfaces.py deleted file mode 100644 index 32e9d4343..000000000 --- a/script.service.hyperion-control/resources/lib/interfaces.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Observer interface.""" -from typing import Protocol - - -class Observer(Protocol): - """Observer interface.""" - - def notify(self, command: str) -> None: - """Process the received command.""" - pass - - -class Logger(Protocol): - """Logger interface.""" - - def log(self, message: str, level: int = 0) -> None: - """Logs a message.""" - - def debug(self, message: str) -> None: - """Writes a debug message to the log.""" - - def info(self, message: str) -> None: - """Writes an info message to the log.""" - - def error(self, message: str) -> None: - """Writes an error message to the log.""" - - -class SettingsManager(Protocol): - """Settings manager interface.""" - - auth_token: str - current_version: str - address: str - port: int - video_mode_enabled: bool - enable_hyperion: bool - disable_hyperion: bool - target_comp: str - screensaver_enabled: bool - video_enabled: bool - audio_enabled: bool - pause_enabled: bool - menu_enabled: bool - show_changelog_on_update: bool - tasks: int - first_run: bool - - @property - def should_display_changelog(self) -> bool: - """Whether the changelog should be displayed.""" - pass - - @property - def base_url(self) -> str: - """Hyperion server JSON RPC base url.""" - pass - - def set_tasks(self, value: int) -> None: - """Sets the tasks to run.""" - - def set_addon_version(self, value: str) -> None: - """Sets the current addon version for changelog checks.""" - - def set_first_run_done(self) -> None: - """Sets the first run settings to false.""" - - def read_settings(self) -> None: - """Read all settings.""" - - -class GuiHandler(Protocol): - """GUI handler interface.""" - - def notify_label(self, label_id: int) -> None: - """Displays a notification with the localized message.""" - - def notify_text(self, message: str, time: int = 3000, icon: str = "info") -> None: - """Displays a notification.""" - - def do_ssdp_discovery(self) -> None: - """Perform the SSDP discovery and lets the user choose the service.""" - - def do_initial_wizard(self) -> None: - """Displays the initial wizard.""" - - def do_changelog_display(self) -> None: - """Displays the changelog.""" - - -class ApiClient(Protocol): - """API client interface.""" - - def send_component_state(self, component: str, state: bool) -> None: - """Sends the component state.""" - - def send_video_mode(self, mode: str) -> None: - """Sends the current video mode.""" diff --git a/script.service.hyperion-control/resources/lib/logger.py b/script.service.hyperion-control/resources/lib/logger.py deleted file mode 100644 index 98dd88688..000000000 --- a/script.service.hyperion-control/resources/lib/logger.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Logging facility.""" -import xbmc - - -class Logger: - """Logging facility for Kodi add-ons.""" - - def __init__(self, addon_name: str) -> None: - self._addon_name = addon_name - - def log(self, message: str, level: int = xbmc.LOGDEBUG) -> None: - """Writes the message to the logger with the addon name as prefix.""" - xbmc.log(f"[{self._addon_name}] - {message}", level=level) - - def debug(self, message: str) -> None: - """Writes a debug message to the log.""" - self.log(message) - - def info(self, message: str) -> None: - """Writes an info message to the log.""" - self.log(message, level=xbmc.LOGINFO) - - def error(self, message: str) -> None: - """Writes an error message to the log.""" - self.log(message, level=xbmc.LOGERROR) diff --git a/script.service.hyperion-control/resources/lib/monitor.py b/script.service.hyperion-control/resources/lib/monitor.py deleted file mode 100644 index 1c26ccbbf..000000000 --- a/script.service.hyperion-control/resources/lib/monitor.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Observable monitor.""" -from __future__ import annotations - -import xbmc - -from resources.lib.interfaces import Observer - - -class XBMCMonitor(xbmc.Monitor): - """xbmc monitor class.""" - - def __init__(self) -> None: - super().__init__() - self._observers: list[Observer] = [] - - def register_observer(self, observer: Observer) -> None: - """Register an observer to the events.""" - self._observers.append(observer) - - def notify_observers(self, command: str) -> None: - """Sends the command to the observers.""" - for observer in self._observers: - observer.notify(command) - - def onSettingsChanged(self) -> None: - """Settings changed event.""" - self.notify_observers("updateSettings") - - def onScreensaverActivated(self) -> None: - """Screensaver activated event.""" - self.notify_observers("screensaver") - - def onScreensaverDeactivated(self) -> None: - """Screensaver deactivated event.""" - self.notify_observers("menu") diff --git a/script.service.hyperion-control/resources/lib/player.py b/script.service.hyperion-control/resources/lib/player.py deleted file mode 100644 index e99993736..000000000 --- a/script.service.hyperion-control/resources/lib/player.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Observable player.""" -from __future__ import annotations - -import xbmc - -from resources.lib.interfaces import Observer - - -class Player(xbmc.Player): - """xbmc player class.""" - - def __init__(self) -> None: - super().__init__() - self._observers: list[Observer] = [] - - def register_observer(self, observer: Observer) -> None: - """Register an observer to the events.""" - self._observers.append(observer) - - def notify_observers(self, command: str) -> None: - """Sends the command to the observers.""" - for observer in self._observers: - observer.notify(command) - - def onPlayBackPaused(self) -> None: - """Playback paused event.""" - self.notify_observers("pause") - - def onPlayBackResumed(self) -> None: - """Playback resumed event.""" - self._play_handler() - - def onAVStarted(self) -> None: - """Audio or Video started event.""" - self._play_handler() - - def onPlayBackStopped(self) -> None: - """Playback stopped event.""" - self.notify_observers("menu") - - def onPlayBackEnded(self) -> None: - """Playback end event.""" - self.notify_observers("menu") - - def _play_handler(self) -> None: - command = "playAudio" if self.isPlayingAudio() else "playVideo" - self.notify_observers(command) diff --git a/script.service.hyperion-control/resources/lib/settings_manager.py b/script.service.hyperion-control/resources/lib/settings_manager.py deleted file mode 100644 index 66b288d49..000000000 --- a/script.service.hyperion-control/resources/lib/settings_manager.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Settings manager.""" -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Any - -from resources.lib.interfaces import Logger - -if TYPE_CHECKING: - import xbmcaddon - - -INT_TO_COMP_STRING = { - 0: "GRABBER", - 1: "V4L", - 2: "AUDIO", - 3: "LEDDEVICE", - 4: "SMOOTHING", - 5: "BLACKBORDER", - 6: "FORWARDER", - 7: "BOBLIGHTSERVER", - 8: "ALL", -} - - -class SettingsManager: - """Class which contains all addon settings.""" - - def __init__( - self, settings: xbmcaddon.Settings, logger: Logger, addon: xbmcaddon.Addon - ) -> None: - self._logger = logger - self.rev = 0 - self._settings = settings - self._addon = addon - self.current_version: str = "" - self._address: str = "localhost" - self._port: int = 8090 - self._base_url = "http://localhost:8090/json-rpc" - self.video_mode_enabled: bool - self.enable_hyperion: bool - self.disable_hyperion: bool - self.auth_token: str - self.target_comp: str - self.screensaver_enabled: bool - self.video_enabled: bool - self.audio_enabled: bool - self.pause_enabled: bool - self.menu_enabled: bool - self.show_changelog_on_update: bool - self.tasks: int - self.first_run: bool - self.read_settings() - - @property - def address(self) -> str: - """Hyperion server address.""" - return self._address - - @address.setter - def address(self, value: str) -> None: - """Hyperion server address.""" - self._address = value - self._update_url() - self._set_string("ip", value) - - @property - def port(self) -> int: - """Hyperion server port.""" - return self._port - - @port.setter - def port(self, value: int) -> None: - """Hyperion server port.""" - self._port = value - self._update_url() - self._set_int("port", value) - - def _update_url(self) -> None: - self._base_url = f"http://{self._address}:{self._port}/json-rpc" - - @property - def base_url(self) -> str: - """Hyperion server JSON RPC base url.""" - return self._base_url - - @property - def should_display_changelog(self) -> bool: - """Whether the changelog should be displayed.""" - return self.show_changelog_on_update and not self.first_run - - def _set_string(self, name: str, value: str) -> None: - self._addon.setSettingString(name, value) - outcome = value == self._settings.getString(name) - self._log_set_outcome(name, value, outcome) - - def _set_int(self, name: str, value: int) -> None: - self._addon.setSettingInt(name, value) - outcome = value == self._settings.getInt(name) - self._log_set_outcome(name, value, outcome) - - def _set_bool(self, name: str, value: bool) -> None: - self._addon.setSettingBool(name, value) - outcome = value == self._settings.getBool(name) - self._log_set_outcome(name, value, outcome) - - def _log_set_outcome(self, name: str, value: Any, outcome: bool) -> None: - if not outcome: - self._logger.error(f"Error setting {name} to {value} (outcome={outcome})") - else: - self._logger.log(f"Set {name} to {value}") - - def set_tasks(self, value: int) -> None: - """Sets the tasks to run.""" - self._set_int("tasks", value) - self.tasks = value - - def set_addon_version(self, value: str) -> None: - """Sets the current addon version for changelog checks.""" - self._set_string("currAddonVersion", value) - self.current_version = value - - def set_first_run_done(self) -> None: - """Sets the first run settings to false.""" - self._set_bool("firstRun", False) - self.first_run = False - - def read_settings(self) -> None: - """Read all settings.""" - settings = self._settings - get_bool = settings.getBool - get_string = settings.getString - get_int = settings.getInt - self._address = get_string("ip") - self._port = get_int("port") - self._update_url() - self.auth_token = get_string("authToken") - self.video_mode_enabled = get_bool("videoModeEnabled") - self.enable_hyperion = get_bool("enableHyperion") - self.disable_hyperion = get_bool("disableHyperion") - self.show_changelog_on_update = get_bool("showChangelogOnUpdate") - self.first_run = get_bool("firstRun") - self.current_version = get_string("currAddonVersion") - - self.target_comp = INT_TO_COMP_STRING.get( - get_int("targetComponent"), "NOT_FOUND" - ) - self.video_enabled = get_bool("videoEnabled") - self.audio_enabled = get_bool("audioEnabled") - self.pause_enabled = get_bool("pauseEnabled") - self.menu_enabled = get_bool("menuEnabled") - self.screensaver_enabled = get_bool("screensaverEnabled") - self.tasks = get_int("tasks") - self.rev += 1 - - self._log_settings() - - def _log_settings(self) -> None: - log = self._logger.log - log("Settings updated!") - log(f"Hyperion ip: {self.address}") - log(f"Hyperion port: {self.port}") - log(f"Enable H on start: {self.enable_hyperion}") - log(f"Disable H on stop: {self.disable_hyperion}") - log(f"VideoMode enabled: {self.video_mode_enabled}") - log(f"Hyperion target comp: {self.target_comp}") - log(f"Screensaver enabled: {self.screensaver_enabled}") - log(f"Video enabled: {self.video_enabled}") - log(f"Audio enabled: {self.audio_enabled}") - log(f"Pause enabled: {self.pause_enabled}") - log(f"Menu enabled: {self.menu_enabled}") - log(f"ChangelogOnUpdate: {self.show_changelog_on_update}") - log(f"tasks: {self.tasks}") - log(f"first run: {self.first_run}") - log(f"current version: {self.current_version}") diff --git a/script.service.hyperion-control/resources/lib/ssdp.py b/script.service.hyperion-control/resources/lib/ssdp.py deleted file mode 100644 index 6340ddbe7..000000000 --- a/script.service.hyperion-control/resources/lib/ssdp.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -Hyperion UPnP / SSDP service discovery. - -Copyright 2014 Dan Krause -Copyright 2023 Andrea Ghensi - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -All credits to Dan Krause at Github: https://gist.github.com/dankrause/6000248 -""" -from __future__ import annotations - -import http.client -import socket -from io import BytesIO -from typing import Any - - -class _FakeSocket(BytesIO): - """Fake socket to make ssdp response compatible with HTTPResponse.""" - - def makefile(self, *_args: Any, **_kw: Any) -> BytesIO: - """Duck-types the call to make the socket available.""" - return self - - -def _parse_location(location: str | None) -> tuple[str, int]: - if location is None: - return "", 0 - start = location.find("//") + 2 - end = location.rfind("/") - hostname, port = location[start:end].split(":") - return hostname, int(port) - - -class SSDPResponse: - """ - Response from SSDP discovery. - - Typical Hyperion response: - - CACHE-CONTROL: max-age = 1800 - DATE: Sun, 23 Jul 2023 19:58:01 G7T19444 - EXT: - LOCATION: http://192.168.2.180:8090/description.xml - SERVER: LibreELEC (official): 11.0.1/11.0 UPnP/1.0 Hyperion/2.0.16-beta.1+PR1617 - ST: urn:hyperion-project.org:device:basic:1 - USN: uuid:04928741-2192-5c24-93e6-638c9a184443 - HYPERION-FBS-PORT: 19400 - HYPERION-JSS-PORT: 19444 - HYPERION-NAME: My Hyperion Config - """ - - def __init__(self, response: bytes) -> None: - r = http.client.HTTPResponse(_FakeSocket(response)) # type: ignore - r.begin() - hostname, port = _parse_location(r.getheader("location")) - self.hostname = hostname - self.port = port - self.usn = r.getheader("usn") - self.st = r.getheader("st") - cache = r.getheader("cache-control") - self.cache = cache.split("=")[1] if cache else "" - - -def discover(timeout: int = 3, retries: int = 1, mx: int = 2) -> list[dict[str, Any]]: - service = "urn:hyperion-project.org:device:basic:1" - group = ("239.255.255.250", 1900) - lines = [ - "M-SEARCH * HTTP/1.1", - f"HOST: {group[0]}:{group[1]}", - 'MAN: "ssdp:discover"', - f"ST: {service}", - f"MX: {mx}", - "", - "", - ] - message = "\r\n".join(lines).encode("utf-8") - socket.setdefaulttimeout(timeout) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - responses = [] - for _ in range(retries): - sock.sendto(message, group) - while True: - try: - res_data = SSDPResponse(sock.recv(1024)) - if res_data.st != service: - continue - responses.append( - { - "ip": res_data.hostname, - "port": res_data.port, - "usn": res_data.usn, - } - ) - except socket.timeout: - break - sock.close() - return responses diff --git a/script.service.hyperion-control/resources/lib/utils.py b/script.service.hyperion-control/resources/lib/utils.py deleted file mode 100644 index 936047003..000000000 --- a/script.service.hyperion-control/resources/lib/utils.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Stereoscopic mode detection.""" -import json - -import xbmc - -from resources.lib.interfaces import Logger - - -def get_stereoscopic_mode(logger: Logger) -> str: - """Returns the currently active stereoscopic mode.""" - msg = { - "jsonrpc": "2.0", - "method": "GUI.GetProperties", - "params": {"properties": ["stereoscopicmode"]}, - "id": 669, - } - try: - response = json.loads(xbmc.executeJSONRPC(json.dumps(msg))) - mode = response["result"]["stereoscopicmode"]["mode"] - except Exception: - logger.error("Error executing JSONRPC call") - return "2D" - if mode == "split_vertical": - return "3DSBS" - return "3DTAB" if mode == "split_horizontal" else "2D" diff --git a/script.service.hyperion-control/resources/screenshot-01.png b/script.service.hyperion-control/resources/screenshot-01.png deleted file mode 100644 index ed4e2c262..000000000 Binary files a/script.service.hyperion-control/resources/screenshot-01.png and /dev/null differ diff --git a/script.service.hyperion-control/resources/settings.xml b/script.service.hyperion-control/resources/settings.xml deleted file mode 100644 index a9323f216..000000000 --- a/script.service.hyperion-control/resources/settings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/script.service.hyperion-control/service.py b/script.service.hyperion-control/service.py deleted file mode 100644 index b6c4afaf0..000000000 --- a/script.service.hyperion-control/service.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Hyperion control addon entrypoint.""" -import xbmcaddon -from resources.lib.api_client import ApiClient -from resources.lib.gui import GuiHandler -from resources.lib.hyperion import Hyperion -from resources.lib.logger import Logger -from resources.lib.monitor import XBMCMonitor -from resources.lib.player import Player -from resources.lib.settings_manager import SettingsManager -from resources.lib.utils import get_stereoscopic_mode - -ADDON_NAME = "script.service.hyperion-control" - - -def main() -> None: - addon = xbmcaddon.Addon(ADDON_NAME) - logger = Logger(addon.getAddonInfo("name")) - settings_manager = SettingsManager(addon.getSettings(), logger, addon) - gui_handler = GuiHandler(addon, settings_manager) - api_client = ApiClient(logger, gui_handler, settings_manager) - - def get_video_mode_fn() -> str: - return get_stereoscopic_mode(logger) - - hyperion = Hyperion( - settings_manager, - logger, - gui_handler, - api_client, - get_video_mode_fn, - addon.getAddonInfo("version"), - ) - player = Player() - player.register_observer(hyperion) - monitor = XBMCMonitor() - monitor.register_observer(hyperion) - - while not monitor.abortRequested(): - if monitor.waitForAbort(10): - hyperion.stop() - break - - -if __name__ == "__main__": - main() diff --git a/script.service.hyperion/addon.xml b/script.service.hyperion/addon.xml deleted file mode 100644 index 553fcc18f..000000000 --- a/script.service.hyperion/addon.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - Capture and send your current Kodi-picture to your Hyperion Ambient Lighting system. - Übermittle das aktuelle Kodi-Bild an dein Hyperion Ambient Lighting System. - Cattura e invia l'immagine corrente di Kodi ad tuo sistema di Ambient Lighting Hyperion. - Hyperion is an opensource Bias or Ambient Lighting implementation which runs on many devices.[CR]This addon sends through the Hyperion PROTO-port your current picture to your Hyperion daemon for further processing like black border detection or smoothing. - Hyperion ist ein Bias oder Ambient Lighting System, welches auf vielen Plattformen lauffähig ist.[CR]Dieses Addon sendet über den PROTO-Port laufend Bilder an Hyperion. - Hyperion è un'implementazione open source di Bias o Ambient Lighting che supporta molti dispositivi.[CR]Questo addon invia l'immagine corrente alla porta PROTO del demone Hyperion per ulteriori elaborazioni come il rilevamento bordi neri o lo smoothing. - all - https://hyperion-project.org/forum - https://www.hyperion-project.org - https://github.com/hyperion-project/hyperion.kodi - MIT - - 20.0.1 - - Clean-ups - - Handle missing PIL library on Android - 20.0.0 - - Kodi 20 (Nexus) support - - New languages: Hebrew, Italian - - - resources/icon.png - resources/fanart.png - resources/screenshot-01.png - - - diff --git a/script.service.hyperion/resources/__init__.py b/script.service.hyperion/resources/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.service.hyperion/resources/fanart.png b/script.service.hyperion/resources/fanart.png deleted file mode 100644 index 3944f0514..000000000 Binary files a/script.service.hyperion/resources/fanart.png and /dev/null differ diff --git a/script.service.hyperion/resources/icon.png b/script.service.hyperion/resources/icon.png deleted file mode 100644 index c68c40a30..000000000 Binary files a/script.service.hyperion/resources/icon.png and /dev/null differ diff --git a/script.service.hyperion/resources/language/resource.language.de_de/strings.po b/script.service.hyperion/resources/language/resource.language.de_de/strings.po deleted file mode 100644 index 82d02c042..000000000 --- a/script.service.hyperion/resources/language/resource.language.de_de/strings.po +++ /dev/null @@ -1,63 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Hyperion Ambilight -# Addon id: script.service.hyperion -# Addon Provider: hyperion-project - -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: hyperion-project.org\n" -"Last-Translator: hyperion-project\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: de-DE\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "#32001" -msgid "General" -msgstr "Allgemein" - -msgctxt "#32002" -msgid "Enable addon (Grabbing)" -msgstr "Aktiviere das Addon (Grabbing)" - -msgctxt "#32003" -msgid "Hyperion hostname or IP-address" -msgstr "Hyperion Hostname oder IP-Adresse" - -msgctxt "#32004" -msgid "Hyperion host PROTO-port" -msgstr "Hyperion Host PROTO-Port" - -msgctxt "#32005" -msgid "Hyperion priority channel" -msgstr "Hyperion Priorität" - -msgctxt "#32006" -msgid "Reconnect timeout (s)" -msgstr "Zeitlimit für erneute Verbindung (s)" - -msgctxt "#32007" -msgid "Enable when on screensaver" -msgstr "Aktiviere bei Bildschirmschoner" - -msgctxt "#32008" -msgid "Grabber picture width (pixel)" -msgstr "Bildbreite (pixel)" - -msgctxt "#32010" -msgid "Frame rate (fps)" -msgstr "Bildfrequenz (fps)" - -msgctxt "#32100" -msgid "Error: Unable to connect to Hyperion. Wrong hostname or IP-address?" -msgstr "Fehler: Kann keine Verbindung zu Hyperion aufbauen. Falscher Hostname oder IP-Adresse?" - -msgctxt "#32101" -msgid "Error: Unable to send image to Hyperion. Wrong PROTO-port?" -msgstr "Fehler: Kann kein Bild an Hyperion senden. Falscher PROTO-Port?" - -msgctxt "#32102" -msgid "ATTENTION: Higher the values below may lead to performance issues" -msgstr "ACHTUNG: Ein Erhöhen der folgenden Werte kann zu hoher CPU-Last führen" diff --git a/script.service.hyperion/resources/language/resource.language.en_gb/strings.po b/script.service.hyperion/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index f52092c6f..000000000 --- a/script.service.hyperion/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,63 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Hyperion Ambilight -# Addon id: script.service.hyperion -# Addon Provider: hyperion-project - -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: hyperion-project.org\n" -"Last-Translator: hyperion-project\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en_GB\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "#32001" -msgid "General" -msgstr "General" - -msgctxt "#32002" -msgid "Enable addon (Grabbing)" -msgstr "Enable addon (Grabbing)" - -msgctxt "#32003" -msgid "Hyperion hostname or IP-address" -msgstr "Hyperion hostname or IP-address" - -msgctxt "#32004" -msgid "Hyperion host PROTO-port" -msgstr "Hyperion host PROTO-port" - -msgctxt "#32005" -msgid "Hyperion priority channel" -msgstr "Hyperion priority channel" - -msgctxt "#32006" -msgid "Reconnect timeout (s)" -msgstr "Reconnect timeout (s)" - -msgctxt "#32007" -msgid "Enable when on screensaver" -msgstr "Enable when on screensaver" - -msgctxt "#32008" -msgid "Grabber picture width (pixel)" -msgstr "Grabber picture width (pixel)" - -msgctxt "#32010" -msgid "Frame rate (fps)" -msgstr "Frame rate (fps)" - -msgctxt "#32100" -msgid "Error: Unable to connect to Hyperion. Wrong hostname or IP-address?" -msgstr "" - -msgctxt "#32101" -msgid "Error: Unable to send image to Hyperion. Wrong PROTO-port?" -msgstr "" - -msgctxt "#32102" -msgid "ATTENTION: Higher the values below may lead to performance issues" -msgstr "" diff --git a/script.service.hyperion/resources/language/resource.language.he_il/strings.po b/script.service.hyperion/resources/language/resource.language.he_il/strings.po deleted file mode 100644 index 9d66c7df7..000000000 --- a/script.service.hyperion/resources/language/resource.language.he_il/strings.po +++ /dev/null @@ -1,66 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Hyperion Ambilight -# Addon id: script.service.hyperion -# Addon Provider: hyperion-project -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: hyperion-project.org\n" -"POT-Creation-Date: 2014-10-26 17:05+0000\n" -"PO-Revision-Date: 2017-10-09 10:05+0300\n" -"Last-Translator: A. Dambledore\n" -"Language-Team: Eng2Heb\n" -"Language: he_IL\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - - -msgctxt "#32001" -msgid "General" -msgstr "כללי" - -msgctxt "#32002" -msgid "Enable addon (Grabbing)" -msgstr "אפשר הרחבה (Grabbing - תפיסה)" - -msgctxt "#32003" -msgid "Hyperion hostname or IP-address" -msgstr "היפריון כתובת ה-IP של המארח" - -msgctxt "#32004" -msgid "Hyperion host PROTO-port" -msgstr "היפריון פורט PROTO של המארח" - -msgctxt "#32005" -msgid "Hyperion priority channel" -msgstr "ערוץ עדיפות היפריון" - -msgctxt "#32006" -msgid "Reconnect timeout (s)" -msgstr "זמן קצוב לחיבור מחדש (ש)" - -msgctxt "#32007" -msgid "Enable when on screensaver" -msgstr "אפשר במצב שומר מסך" - -msgctxt "#32008" -msgid "Grabber picture width (pixel)" -msgstr "תופש התמונות רוחב (פיקסלים)" - -msgctxt "#32010" -msgid "Frame rate (fps)" -msgstr "קצב הפריימים (fps)" - -msgctxt "#32100" -msgid "Error: Unable to connect to Hyperion. Wrong hostname or IP-address?" -msgstr "שגיאה: אין אפשרות להתחבר אל היפריון. כתובת ה-IP שגויה?" - -msgctxt "#32101" -msgid "Error: Unable to send image to Hyperion. Wrong PROTO-port?" -msgstr "שגיאה: אין אפשרות להתחבר אל היפריון. פורט PROTO שגוי?" - -msgctxt "#32102" -msgid "ATTENTION: Higher the values below may lead to performance issues" -msgstr "שימו לב: הערכים הגבוהים שלהלן עלולים להוביל לבעיות ביצועים" diff --git a/script.service.hyperion/resources/language/resource.language.it_it/strings.po b/script.service.hyperion/resources/language/resource.language.it_it/strings.po deleted file mode 100644 index b893343ed..000000000 --- a/script.service.hyperion/resources/language/resource.language.it_it/strings.po +++ /dev/null @@ -1,63 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Hyperion Ambilight -# Addon id: script.service.hyperion -# Addon Provider: hyperion-project - -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: hyperion-project.org\n" -"Last-Translator: hyperion-project\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: it-IT\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "#32001" -msgid "General" -msgstr "Generale" - -msgctxt "#32002" -msgid "Enable addon (Grabbing)" -msgstr "Abilita l'Addon (Acquisizione)" - -msgctxt "#32003" -msgid "Hyperion hostname or IP-address" -msgstr "Nome host o indirizzo IP di Hyperion" - -msgctxt "#32004" -msgid "Hyperion host PROTO-port" -msgstr "Porta PROTO di Hyperion" - -msgctxt "#32005" -msgid "Hyperion priority channel" -msgstr "Canale prioritario di Hyperion" - -msgctxt "#32006" -msgid "Reconnect timeout (s)" -msgstr "Timeout riconnessione (s)" - -msgctxt "#32007" -msgid "Enable when on screensaver" -msgstr "Abilita con lo screen saver" - -msgctxt "#32008" -msgid "Grabber picture width (pixel)" -msgstr "Larghezza immagine acquisita (pixel)" - -msgctxt "#32010" -msgid "Frame rate (fps)" -msgstr "Frame rate (fps)" - -msgctxt "#32100" -msgid "Error: Unable to connect to Hyperion. Wrong hostname or IP-address?" -msgstr "Errore: Impossible connettersi a Hyperion. Nome host o indirizzo IP sbagliato?" - -msgctxt "#32101" -msgid "Error: Unable to send image to Hyperion. Wrong PROTO-port?" -msgstr "Errore: Impossibile inviare l'immagine a Hyperion. porta PROTO sbagliata?" - -msgctxt "#32102" -msgid "ATTENTION: Higher the values below may lead to performance issues" -msgstr "ATTENZIONE: Alzare i valori seguenti può portare a problemi di performance" diff --git a/script.service.hyperion/resources/lib/__init__.py b/script.service.hyperion/resources/lib/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.service.hyperion/resources/lib/gui.py b/script.service.hyperion/resources/lib/gui.py deleted file mode 100644 index 4ed312594..000000000 --- a/script.service.hyperion/resources/lib/gui.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Kodi GUI handler.""" -from __future__ import annotations - -import xbmcaddon -import xbmcgui -from resources.lib.settings import SettingsManager - - -class GuiHandler: - """Kodi GUI handler.""" - - def __init__( - self, addon: xbmcaddon.Addon, settings_manager: SettingsManager - ) -> None: - self._addon = addon - self._settings = settings_manager - self._dialog = xbmcgui.Dialog() # TODO: DI with embedded getlocalizedstring - self._addon_name = addon.getAddonInfo("name") - self._addon_icon = addon.getAddonInfo("icon") - - def _get_localized_string(self, label_id: int) -> str: - """Returns the localized string of a label ID.""" - return self._addon.getLocalizedString(label_id) - - def notify_label(self, label_id: int) -> None: - """Displays a notification with the localized message.""" - message = self._get_localized_string(label_id) - self.notify_text(message, time=1000, icon=self._addon_icon) - - def notify_text( - self, message: str, time: int = 3000, icon: str = xbmcgui.NOTIFICATION_INFO - ) -> None: - """Displays a notification.""" - self._dialog.notification(self._addon_name, message, icon, time) diff --git a/script.service.hyperion/resources/lib/hyperion/__init__.py b/script.service.hyperion/resources/lib/hyperion/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.service.hyperion/resources/lib/hyperion/hyperion.py b/script.service.hyperion/resources/lib/hyperion/hyperion.py deleted file mode 100644 index 159791457..000000000 --- a/script.service.hyperion/resources/lib/hyperion/hyperion.py +++ /dev/null @@ -1,146 +0,0 @@ -""" -Kodi video capturer for Hyperion. - -Copyright (c) 2013-2023 Hyperion Team - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -# mypy: ignore-errors -import socket -import struct -from typing import Any - -from .message_pb2 import ClearRequest -from .message_pb2 import ColorRequest -from .message_pb2 import HyperionReply -from .message_pb2 import HyperionRequest -from .message_pb2 import ImageRequest - - -class Hyperion: - """ - Hyperion connection class. - - A Hyperion object will connect to the Hyperion server and provide - easy to use functions to send requests - - Note that the function will block until a reply has been received - from the Hyperion server (or the call has timed out) - """ - - def __init__(self, server: str, port: int) -> None: - """ - Constructor. - - Args: - server: server address of Hyperion - port: port number of Hyperion - """ - # create a new socket - self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._socket.settimeout(2) - - # Connect socket to the provided server - self._socket.connect((server, port)) - - def __del__(self) -> None: - """Destructor.""" - # close the socket - self._socket.close() - - def send_color(self, color: int, priority: int, duration: int = -1) -> None: - """ - Send a static color to Hyperion. - - Args: - color: integer value with the color as 0x00RRGGBB - priority: the priority channel to use - duration: duration the leds should be set - """ - request = HyperionRequest() - request.command = HyperionRequest.COLOR - color_request = request.Extensions[ColorRequest.colorRequest] - color_request.rgbColor = color - color_request.priority = priority - color_request.duration = duration - self._send_message(request) - - def send_image( - self, width: int, height: int, data: bytes, priority: int, duration: int = -1 - ) -> None: - """ - Send an image to Hyperion. - - Args: - width: width of the image - height: height of the image - data: image data (byte string containing 0xRRGGBB pixel values) - priority: the priority channel to use - duration: duration the leds should be set - """ - request = HyperionRequest() - request.command = HyperionRequest.IMAGE - image_request = request.Extensions[ImageRequest.imageRequest] - image_request.imagewidth = width - image_request.imageheight = height - image_request.imagedata = bytes(data) - image_request.priority = priority - image_request.duration = duration - self._send_message(request) - - def clear(self, priority: int) -> None: - """Clear the given priority channel. - - Args: - priority: the priority channel to clear - """ - request = HyperionRequest() - request.command = HyperionRequest.CLEAR - clear_request = request.Extensions[ClearRequest.clearRequest] - clear_request.priority = priority - self._send_message(request) - - def clear_all(self) -> None: - """Clear all active priority channels.""" - request = HyperionRequest() - request.command = HyperionRequest.CLEARALL - self._send_message(request) - - def _send_message(self, message: Any) -> None: - """ - Send the given proto message to Hyperion. - - A RuntimeError will be raised if the reply contains an error - - Args: - message : proto request to send - """ - binary_request = message.SerializeToString() - binary_size = struct.pack(">I", len(binary_request)) - self._socket.sendall(binary_size) - self._socket.sendall(binary_request) - - # receive a reply from Hyperion - size = struct.unpack(">I", self._socket.recv(4))[0] - reply = HyperionReply() - reply.ParseFromString(self._socket.recv(size)) - - # check the reply - if not reply.success: - raise RuntimeError(f"Hyperion server error: {reply.error}") diff --git a/script.service.hyperion/resources/lib/hyperion/message.proto b/script.service.hyperion/resources/lib/hyperion/message.proto deleted file mode 100644 index 94a2c92c9..000000000 --- a/script.service.hyperion/resources/lib/hyperion/message.proto +++ /dev/null @@ -1,82 +0,0 @@ -syntax = "proto2"; - -package proto; - -message HyperionRequest { - enum Command { - COLOR = 1; - IMAGE = 2; - CLEAR = 3; - CLEARALL = 4; - } - - // command specification - required Command command = 1; - - // extensions to define all specific requests - extensions 10 to 100; -} - -message ColorRequest { - extend HyperionRequest { - optional ColorRequest colorRequest = 10; - } - - // priority to use when setting the color - required int32 priority = 1; - - // integer value containing the rgb color (0x00RRGGBB) - required int32 RgbColor = 2; - - // duration of the request (negative results in infinite) - optional int32 duration = 3; -} - -message ImageRequest { - extend HyperionRequest { - optional ImageRequest imageRequest = 11; - } - - // priority to use when setting the image - required int32 priority = 1; - - // width of the image - required int32 imagewidth = 2; - - // height of the image - required int32 imageheight = 3; - - // image data - required bytes imagedata = 4; - - // duration of the request (negative results in infinite) - optional int32 duration = 5; -} - -message ClearRequest { - extend HyperionRequest { - optional ClearRequest clearRequest = 12; - } - - // priority which need to be cleared - required int32 priority = 1; -} - -message HyperionReply { - enum Type { - REPLY = 1; - VIDEO = 2; - } - - // Identifies which field is filled in. - required Type type = 1; - - // flag indication success or failure - optional bool success = 2; - - // string indicating the reason for failure (if applicable) - optional string error = 3; - - // Proto Messages for video mode - optional int32 video = 4; -} diff --git a/script.service.hyperion/resources/lib/hyperion/message_pb2.py b/script.service.hyperion/resources/lib/hyperion/message_pb2.py deleted file mode 100644 index 707b36c73..000000000 --- a/script.service.hyperion/resources/lib/hyperion/message_pb2.py +++ /dev/null @@ -1,617 +0,0 @@ -# mypy: ignore-errors -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: message.proto -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor.FileDescriptor( - name="message.proto", - package="proto", - syntax="proto2", - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\rmessage.proto\x12\x05proto"\x82\x01\n\x0fHyperionRequest\x12/\n\x07\x63ommand\x18\x01 \x02(\x0e\x32\x1e.proto.HyperionRequest.Command"8\n\x07\x43ommand\x12\t\n\x05\x43OLOR\x10\x01\x12\t\n\x05IMAGE\x10\x02\x12\t\n\x05\x43LEAR\x10\x03\x12\x0c\n\x08\x43LEARALL\x10\x04*\x04\x08\n\x10\x65"\x87\x01\n\x0c\x43olorRequest\x12\x10\n\x08priority\x18\x01 \x02(\x05\x12\x10\n\x08RgbColor\x18\x02 \x02(\x05\x12\x10\n\x08\x64uration\x18\x03 \x01(\x05\x32\x41\n\x0c\x63olorRequest\x12\x16.proto.HyperionRequest\x18\n \x01(\x0b\x32\x13.proto.ColorRequest"\xb1\x01\n\x0cImageRequest\x12\x10\n\x08priority\x18\x01 \x02(\x05\x12\x12\n\nimagewidth\x18\x02 \x02(\x05\x12\x13\n\x0bimageheight\x18\x03 \x02(\x05\x12\x11\n\timagedata\x18\x04 \x02(\x0c\x12\x10\n\x08\x64uration\x18\x05 \x01(\x05\x32\x41\n\x0cimageRequest\x12\x16.proto.HyperionRequest\x18\x0b \x01(\x0b\x32\x13.proto.ImageRequest"c\n\x0c\x43learRequest\x12\x10\n\x08priority\x18\x01 \x02(\x05\x32\x41\n\x0c\x63learRequest\x12\x16.proto.HyperionRequest\x18\x0c \x01(\x0b\x32\x13.proto.ClearRequest"\x85\x01\n\rHyperionReply\x12\'\n\x04type\x18\x01 \x02(\x0e\x32\x19.proto.HyperionReply.Type\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\r\n\x05video\x18\x04 \x01(\x05"\x1c\n\x04Type\x12\t\n\x05REPLY\x10\x01\x12\t\n\x05VIDEO\x10\x02', -) - - -_HYPERIONREQUEST_COMMAND = _descriptor.EnumDescriptor( - name="Command", - full_name="proto.HyperionRequest.Command", - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name="COLOR", - index=0, - number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.EnumValueDescriptor( - name="IMAGE", - index=1, - number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.EnumValueDescriptor( - name="CLEAR", - index=2, - number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.EnumValueDescriptor( - name="CLEARALL", - index=3, - number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key, - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=93, - serialized_end=149, -) -_sym_db.RegisterEnumDescriptor(_HYPERIONREQUEST_COMMAND) - -_HYPERIONREPLY_TYPE = _descriptor.EnumDescriptor( - name="Type", - full_name="proto.HyperionReply.Type", - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name="REPLY", - index=0, - number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.EnumValueDescriptor( - name="VIDEO", - index=1, - number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key, - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=682, - serialized_end=710, -) -_sym_db.RegisterEnumDescriptor(_HYPERIONREPLY_TYPE) - - -_HYPERIONREQUEST = _descriptor.Descriptor( - name="HyperionRequest", - full_name="proto.HyperionRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name="command", - full_name="proto.HyperionRequest.command", - index=0, - number=1, - type=14, - cpp_type=8, - label=2, - has_default_value=False, - default_value=1, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - extensions=[], - nested_types=[], - enum_types=[ - _HYPERIONREQUEST_COMMAND, - ], - serialized_options=None, - is_extendable=True, - syntax="proto2", - extension_ranges=[ - (10, 101), - ], - oneofs=[], - serialized_start=25, - serialized_end=155, -) - - -_COLORREQUEST = _descriptor.Descriptor( - name="ColorRequest", - full_name="proto.ColorRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name="priority", - full_name="proto.ColorRequest.priority", - index=0, - number=1, - type=5, - cpp_type=1, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="RgbColor", - full_name="proto.ColorRequest.RgbColor", - index=1, - number=2, - type=5, - cpp_type=1, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="duration", - full_name="proto.ColorRequest.duration", - index=2, - number=3, - type=5, - cpp_type=1, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - extensions=[ - _descriptor.FieldDescriptor( - name="colorRequest", - full_name="proto.ColorRequest.colorRequest", - index=0, - number=10, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=158, - serialized_end=293, -) - - -_IMAGEREQUEST = _descriptor.Descriptor( - name="ImageRequest", - full_name="proto.ImageRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name="priority", - full_name="proto.ImageRequest.priority", - index=0, - number=1, - type=5, - cpp_type=1, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="imagewidth", - full_name="proto.ImageRequest.imagewidth", - index=1, - number=2, - type=5, - cpp_type=1, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="imageheight", - full_name="proto.ImageRequest.imageheight", - index=2, - number=3, - type=5, - cpp_type=1, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="imagedata", - full_name="proto.ImageRequest.imagedata", - index=3, - number=4, - type=12, - cpp_type=9, - label=2, - has_default_value=False, - default_value=b"", - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="duration", - full_name="proto.ImageRequest.duration", - index=4, - number=5, - type=5, - cpp_type=1, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - extensions=[ - _descriptor.FieldDescriptor( - name="imageRequest", - full_name="proto.ImageRequest.imageRequest", - index=0, - number=11, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=296, - serialized_end=473, -) - - -_CLEARREQUEST = _descriptor.Descriptor( - name="ClearRequest", - full_name="proto.ClearRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name="priority", - full_name="proto.ClearRequest.priority", - index=0, - number=1, - type=5, - cpp_type=1, - label=2, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - extensions=[ - _descriptor.FieldDescriptor( - name="clearRequest", - full_name="proto.ClearRequest.clearRequest", - index=0, - number=12, - type=11, - cpp_type=10, - label=1, - has_default_value=False, - default_value=None, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=475, - serialized_end=574, -) - - -_HYPERIONREPLY = _descriptor.Descriptor( - name="HyperionReply", - full_name="proto.HyperionReply", - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name="type", - full_name="proto.HyperionReply.type", - index=0, - number=1, - type=14, - cpp_type=8, - label=2, - has_default_value=False, - default_value=1, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="success", - full_name="proto.HyperionReply.success", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="error", - full_name="proto.HyperionReply.error", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=b"".decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - _descriptor.FieldDescriptor( - name="video", - full_name="proto.HyperionReply.video", - index=3, - number=4, - type=5, - cpp_type=1, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - ), - ], - extensions=[], - nested_types=[], - enum_types=[ - _HYPERIONREPLY_TYPE, - ], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=577, - serialized_end=710, -) - -_HYPERIONREQUEST.fields_by_name["command"].enum_type = _HYPERIONREQUEST_COMMAND -_HYPERIONREQUEST_COMMAND.containing_type = _HYPERIONREQUEST -_HYPERIONREPLY.fields_by_name["type"].enum_type = _HYPERIONREPLY_TYPE -_HYPERIONREPLY_TYPE.containing_type = _HYPERIONREPLY -DESCRIPTOR.message_types_by_name["HyperionRequest"] = _HYPERIONREQUEST -DESCRIPTOR.message_types_by_name["ColorRequest"] = _COLORREQUEST -DESCRIPTOR.message_types_by_name["ImageRequest"] = _IMAGEREQUEST -DESCRIPTOR.message_types_by_name["ClearRequest"] = _CLEARREQUEST -DESCRIPTOR.message_types_by_name["HyperionReply"] = _HYPERIONREPLY -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -HyperionRequest = _reflection.GeneratedProtocolMessageType( - "HyperionRequest", - (_message.Message,), - { - "DESCRIPTOR": _HYPERIONREQUEST, - "__module__": "message_pb2" - # @@protoc_insertion_point(class_scope:proto.HyperionRequest) - }, -) -_sym_db.RegisterMessage(HyperionRequest) - -ColorRequest = _reflection.GeneratedProtocolMessageType( - "ColorRequest", - (_message.Message,), - { - "DESCRIPTOR": _COLORREQUEST, - "__module__": "message_pb2" - # @@protoc_insertion_point(class_scope:proto.ColorRequest) - }, -) -_sym_db.RegisterMessage(ColorRequest) - -ImageRequest = _reflection.GeneratedProtocolMessageType( - "ImageRequest", - (_message.Message,), - { - "DESCRIPTOR": _IMAGEREQUEST, - "__module__": "message_pb2" - # @@protoc_insertion_point(class_scope:proto.ImageRequest) - }, -) -_sym_db.RegisterMessage(ImageRequest) - -ClearRequest = _reflection.GeneratedProtocolMessageType( - "ClearRequest", - (_message.Message,), - { - "DESCRIPTOR": _CLEARREQUEST, - "__module__": "message_pb2" - # @@protoc_insertion_point(class_scope:proto.ClearRequest) - }, -) -_sym_db.RegisterMessage(ClearRequest) - -HyperionReply = _reflection.GeneratedProtocolMessageType( - "HyperionReply", - (_message.Message,), - { - "DESCRIPTOR": _HYPERIONREPLY, - "__module__": "message_pb2" - # @@protoc_insertion_point(class_scope:proto.HyperionReply) - }, -) -_sym_db.RegisterMessage(HyperionReply) - -_COLORREQUEST.extensions_by_name["colorRequest"].message_type = _COLORREQUEST -HyperionRequest.RegisterExtension(_COLORREQUEST.extensions_by_name["colorRequest"]) -_IMAGEREQUEST.extensions_by_name["imageRequest"].message_type = _IMAGEREQUEST -HyperionRequest.RegisterExtension(_IMAGEREQUEST.extensions_by_name["imageRequest"]) -_CLEARREQUEST.extensions_by_name["clearRequest"].message_type = _CLEARREQUEST -HyperionRequest.RegisterExtension(_CLEARREQUEST.extensions_by_name["clearRequest"]) - -# @@protoc_insertion_point(module_scope) diff --git a/script.service.hyperion/resources/lib/logger.py b/script.service.hyperion/resources/lib/logger.py deleted file mode 100644 index 98dd88688..000000000 --- a/script.service.hyperion/resources/lib/logger.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Logging facility.""" -import xbmc - - -class Logger: - """Logging facility for Kodi add-ons.""" - - def __init__(self, addon_name: str) -> None: - self._addon_name = addon_name - - def log(self, message: str, level: int = xbmc.LOGDEBUG) -> None: - """Writes the message to the logger with the addon name as prefix.""" - xbmc.log(f"[{self._addon_name}] - {message}", level=level) - - def debug(self, message: str) -> None: - """Writes a debug message to the log.""" - self.log(message) - - def info(self, message: str) -> None: - """Writes an info message to the log.""" - self.log(message, level=xbmc.LOGINFO) - - def error(self, message: str) -> None: - """Writes an error message to the log.""" - self.log(message, level=xbmc.LOGERROR) diff --git a/script.service.hyperion/resources/lib/monitor.py b/script.service.hyperion/resources/lib/monitor.py deleted file mode 100644 index fe733b94a..000000000 --- a/script.service.hyperion/resources/lib/monitor.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -Kodi video capturer for Hyperion. - -Copyright (c) 2013-2023 Hyperion Team - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from typing import Callable - -import xbmc -from PIL import Image - -from resources.lib.gui import GuiHandler -from resources.lib.hyperion.hyperion import Hyperion -from resources.lib.logger import Logger -from resources.lib.settings import SettingsManager - -State = Callable[[], "State"] - - -class HyperionMonitor(xbmc.Monitor): - """Class to capture changes in settings and screensaver state.""" - - def __init__( - self, - settings: SettingsManager, - player: xbmc.Player, - output_handler: GuiHandler, - logger: Logger, - ) -> None: - super().__init__() - self.settings = settings - self.output_handler = output_handler - self._logger = logger - self._screensaver = xbmc.getCondVisibility("System.ScreenSaverActive") - self._player = player - self.show_error_message = True - self._hyperion: Hyperion - self._capture: xbmc.RenderCapture - - def onSettingsChanged(self) -> None: - self.settings.read_settings() - if self.grabbing: - self.connect() - - def onScreensaverDeactivated(self) -> None: - self._screensaver = False - - def onScreensaverActivated(self) -> None: - self._screensaver = True - - # TODO: onDPMSActivated/Deactivated when entering/exiting energy saving - - @property - def grabbing(self) -> bool: - """Checks if grabbing is requested based on the current state and settings.""" - return ( - self.settings.enable - and self._player.isPlayingVideo() - and (self.settings.enable_screensaver or not self._screensaver) - ) - - def notify_error(self, label_id: int) -> None: - if self.show_error_message: - self.output_handler.notify_label(label_id) - self.show_error_message = False - - def main_loop(self) -> None: - state = self.disconnected_state - while not self.abortRequested(): - state = state() - - def disconnected_state(self) -> State: - if not self.grabbing: - xbmc.sleep(500) - return self.disconnected_state - try: - self.connect() - return self.connected_state - except Exception: - self.notify_error(32100) - return self.error_state - - def error_state(self) -> State: - rev = self.settings.rev - for _ in range(self.settings.timeout): - if rev != self.settings.rev: - break - if self.waitForAbort(1): - return self.error_state - return self.disconnected_state - - def connect(self) -> None: - settings = self.settings - self._logger.info( - f"Establishing connection to hyperion at {settings.address}:{settings.port}" - ) - self._hyperion = Hyperion(settings.address, settings.port) - self._capture = xbmc.RenderCapture() - - def get_capture_size(self) -> tuple[tuple[int, int], int]: - width = self.settings.capture_width - aspect_ratio = self._capture.getAspectRatio() - height = int(width / aspect_ratio) - capture_size = width, height - expected_capture_size = width * height * 4 # size * 4 bytes - RGBA - return capture_size, expected_capture_size - - def connected_state(self) -> State: - if not self.grabbing: - del self._hyperion - return self.disconnected_state - - capture_size, expected_capture_size = self.get_capture_size() - self._capture.capture(*capture_size) - cap_image = self._capture.getImage(self.settings.sleep_time) - if cap_image is None or len(cap_image) < expected_capture_size: - self._logger.debug( - f"Captured image is none or < expected. " - f"captured: {len(cap_image) if cap_image is not None else 'None'}, " - f"expected: {expected_capture_size}" - ) - xbmc.sleep(250) - return self.connected_state - - # v17+ use BGRA format, converting to RGB - image = Image.frombytes("RGB", capture_size, bytes(cap_image), "raw", "BGRX") - - try: - # send image to hyperion - self._hyperion.send_image( - image.width, - image.height, - image.tobytes(), - self.settings.priority, - self.settings.sleep_time, - ) - except Exception: - # unable to send image. notify and go to the error state - self.output_handler.notify_label(32101) - return self.error_state - - return self.connected_state diff --git a/script.service.hyperion/resources/lib/settings.py b/script.service.hyperion/resources/lib/settings.py deleted file mode 100644 index 3c777404c..000000000 --- a/script.service.hyperion/resources/lib/settings.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Kodi video capturer for Hyperion. - -Copyright (c) 2013-2023 Hyperion Team - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from __future__ import annotations - -from typing import TYPE_CHECKING - -from resources.lib.logger import Logger - -if TYPE_CHECKING: - import xbmcaddon - - -class SettingsManager: - """Class which contains all addon settings.""" - - def __init__(self, settings: xbmcaddon.Settings, logger: Logger) -> None: - self._logger = logger - self.rev = 0 - self._settings = settings - self.address = "localhost" - self.port = 19445 - self.enable: bool - self.enable_screensaver: bool - self.priority: int - self.timeout: int - self.capture_width: int - self.framerate: int - self.sleep_time: int - - self.read_settings() - - def read_settings(self) -> None: - """Read all settings.""" - settings = self._settings - self.enable = settings.getBool("hyperion_enable") - self.enable_screensaver = settings.getBool("screensaver_enable") - self.priority = settings.getInt("hyperion_priority") - self.timeout = settings.getInt("reconnect_timeout") - self.capture_width = settings.getInt("capture_width") - self.framerate = settings.getInt("framerate") - self.sleep_time = int(1.0 / self.framerate * 1000) - self.address = settings.getString("hyperion_ip") - self.port = settings.getInt("hyperion_port") - self.rev += 1 - self._log_settings() - - def _log_settings(self) -> None: - log = self._logger.debug - log("Settings updated!") - log(f"Hyperion ip: {self.address}") - log(f"Hyperion port: {self.port}") - log(f"enabled: {self.enable}") - log(f"enabled on screensaver: {self.enable_screensaver}") - log(f"priority: {self.priority}") - log(f"timeout: {self.timeout}") - log(f"capture width: {self.capture_width}") - log(f"framerate: {self.framerate}") diff --git a/script.service.hyperion/resources/screenshot-01.png b/script.service.hyperion/resources/screenshot-01.png deleted file mode 100644 index 5b3fccb4f..000000000 Binary files a/script.service.hyperion/resources/screenshot-01.png and /dev/null differ diff --git a/script.service.hyperion/resources/settings.xml b/script.service.hyperion/resources/settings.xml deleted file mode 100644 index 2fe81ccb1..000000000 --- a/script.service.hyperion/resources/settings.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/script.service.hyperion/service.py b/script.service.hyperion/service.py deleted file mode 100644 index 3a02c59c3..000000000 --- a/script.service.hyperion/service.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Kodi video capturer for Hyperion. - -Copyright (c) 2013-2023 Hyperion Team - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -import xbmc -import xbmcaddon -from resources.lib.gui import GuiHandler -from resources.lib.logger import Logger -from resources.lib.monitor import HyperionMonitor -from resources.lib.settings import SettingsManager - -ADDON_NAME = "script.service.hyperion" - - -def main() -> None: - addon = xbmcaddon.Addon(ADDON_NAME) - logger = Logger(addon.getAddonInfo("name")) - settings_manager = SettingsManager(addon.getSettings(), logger) - player = xbmc.Player() - output_handler = GuiHandler(addon, settings_manager) - monitor = HyperionMonitor(settings_manager, player, output_handler, logger) - monitor.main_loop() - - -if __name__ == "__main__": - main() diff --git a/script.wikipedia/LICENSE.txt b/script.wikipedia/LICENSE.txt deleted file mode 100644 index f288702d2..000000000 --- a/script.wikipedia/LICENSE.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -them 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 prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey 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; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - 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. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -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. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/script.wikipedia/README.md b/script.wikipedia/README.md deleted file mode 100644 index d507902d2..000000000 --- a/script.wikipedia/README.md +++ /dev/null @@ -1,72 +0,0 @@ -## Wikipedia Scripted Window -Create the following xml dialog in your skin - -``` -script-wikipedia.xml -``` - -```xml - - - 9902 - - - - -``` - -## Required Controls - -| ID | Type | Description | -| :--- | :--- | :--- | -| 9901 | label | Title of the Wikipedia article | -| 9902 | list | List of sections containing the table of contents. Scrolling the list changes text loaded in the section textbox 9903. Clicking a listitem opens a select dialog for page links in the section | -| 9903 | textbox | Textbox with the text of the section focused in 9902 | -| 9904 | label | Creative commons attribution label. REQUIRED! You MUST display this text as per Wikipedia licensing requirements | -| 9905 | image | Creative commons licence logo. The license logo is optional as long as you display the attribution label | -| 61 | scrollbar | Page control for section textbox. Clicking the scrollbar will pop-up select dialog for page links in the section | - -## Window Properties - -| Property | Description | -| :--- | :--- | -| Window.Property(Backdrop) | First result found on wikimedia commons for search term with landscape aspect. Not always accurate but can be useful to use in place of background fanart. This image is aliased to Window(Home).Property(Wikipedia.Backdrop) if you need to use it in a window underneath the dialog. | -| Window.Property(Image) | If the current section has an image, the first image will be added to this property. Otherwise it gets the first image for the page | -| Window.Property(ImageText) | The alt text for the image which is displayed on wikipedia for vision impaired users or when the image does not load | - - -## Changing Tag Formatting - -HTML tag text formatting can be modified using a skin string. - -Example: Change all `` hyperlinks to red -`Skin.SetString(Wikipedia.Format.Link,[COLOR=red]{}[/COLOR])` - -The `{}` curly braces will be replaced with the text between the HTML tags - -| Skin String | Affected HTML Tags | -| :--- | :--- | -| Wikipedia.Format.Link | `` | -| Wikipedia.Format.Bold | ` and class:mw-headline` | -| Wikipedia.Format.Emphasis | ` ` | -| Wikipedia.Format.Superscript | `` | - -## Opening / Searching - -``` -RunScript(script.wikipedia,wikipedia=SEARCHTERM) -``` - -Searches wikipedia for the search term and provides a select dialog for user to select from results. - -Optionally can add a `tmdb_type=TYPE` param to tailor the search for more relevant results -Optionally can add a `xml_file=FILENAME` param to use alternate skin template -Optionally can add a `language=CODE` param to set language with two letter ISO 639-1 code - -Supported language codes: `it` `de` `fr` `es` `en` - -``` -RunScript(script.wikipedia,wikipedia=Alien,tmdb_type=movie,language=en) -RunScript(script.wikipedia,wikipedia=Chernobyl,tmdb_type=tv,language=en) -RunScript(script.wikipedia,wikipedia=Matt Smith,tmdb_type=person,language=en) -``` diff --git a/script.wikipedia/addon.xml b/script.wikipedia/addon.xml deleted file mode 100644 index 16c557139..000000000 --- a/script.wikipedia/addon.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - Access Wikipedia via Kodi - Access Wikipedia via Kodi - GPL-3.0-or-later - - icon.png - fanart.jpg - - - diff --git a/script.wikipedia/fanart.jpg b/script.wikipedia/fanart.jpg deleted file mode 100644 index 50a223606..000000000 Binary files a/script.wikipedia/fanart.jpg and /dev/null differ diff --git a/script.wikipedia/icon.png b/script.wikipedia/icon.png deleted file mode 100644 index cde56dd75..000000000 Binary files a/script.wikipedia/icon.png and /dev/null differ diff --git a/script.wikipedia/resources/language/resource.language.en_gb/strings.po b/script.wikipedia/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index 72b2596c3..000000000 --- a/script.wikipedia/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,27 +0,0 @@ -# XBMC Media Center language file -# Addon Name: Skin Variables -# Addon id: script.skinvariables -# Addon Provider: jurialmunkey -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2014-10-26 17:05+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -#: /resources/lib/api.py -msgctxt "#32001" -msgid "Text from Wikipedia under CC BY-SA 3.0 license." -msgstr "" - -#: /resources/lib/api.py -msgctxt "#32000" -msgid "Unable to parse information in this section" -msgstr "" diff --git a/script.wikipedia/resources/lib/api.py b/script.wikipedia/resources/lib/api.py deleted file mode 100644 index 5ccde0cd6..000000000 --- a/script.wikipedia/resources/lib/api.py +++ /dev/null @@ -1,378 +0,0 @@ -import re -import xbmc -import xbmcgui -from bs4 import BeautifulSoup, Comment -from jurialmunkey.thread import ParallelThread -from jurialmunkey.dialog import BusyDialog -from jurialmunkey.reqapi import RequestAPI -from jurialmunkey.plugin import KodiPlugin - - -KODIPLUGIN = KodiPlugin('script.wikipedia') -get_localized = KODIPLUGIN.get_localized - -ADDONDATA = 'special://profile/addon_data/script.wikipedia/' -get_infolabel = xbmc.getInfoLabel - - -WIKI_SCRL_ID = 61 -WIKI_NAME_ID = 9901 -WIKI_LIST_ID = 9902 -WIKI_TEXT_ID = 9903 -WIKI_ATTR_ID = 9904 -WIKI_CCIM_ID = 9905 -WIKI_ATTRIBUTION = f'{get_localized(32001)}\n{{}}' -WIKI_CCBYSA_IMG = 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/CC_BY-SA_icon.svg/320px-CC_BY-SA_icon.svg.png' -WIKI_UNABLE_TO_PARSE_TEXT = f'*** {get_localized(32000)} ***' - -ACTION_CLOSEWINDOW = (9, 10, 92, 216, 247, 257, 275, 61467, 61448,) -ACTION_MOVEMENT = (1, 2, 3, 4, ) -ACTION_SELECT = (7, ) - -WIKI_LANGUAGE = {'it': 'it', 'de': 'de', 'en': 'en', 'fr': 'fr', 'es': 'es'} -DEFAULT_WIKI_LANGUAGE = 'en' - -WIKI_TAG_LINK = '[COLOR=BF55DDFF]{}[/COLOR]' -WIKI_TAG_BOLD = '[B]{}[/B]' -WIKI_TAG_EMPH = '[LIGHT][I]{} [/I][/LIGHT]' -WIKI_TAG_SUPS = '[LIGHT]{}[/LIGHT]' - - -AFFIXES = { - 'tv': { - 'regex': r'\(TV series\)$', - 'affix': 'television series' - }, - 'movie': { - 'regex': r'\(Film\)$', - 'affix': 'film' - }, - 'person': { - 'regex': r'\(.*\)$', - 'affix': 'born' - }, -} - - -class WikimediaAPI(RequestAPI): - def __init__(self): - super(WikimediaAPI, self).__init__( - req_api_name='Wikimedia', - req_api_url='https://commons.m.wikimedia.org/w/api.php') - - def get_titles(self, query): - params = { - 'action': 'query', 'list': 'search', 'format': 'json', - 'srsearch': f'File: {query}'} - data = self.get_request_lc(**params) - if not data: - return - return [i['title'] for i in data['query']['search'] if i.get('title')] - - def get_images(self, titles): - params = { - 'action': 'query', 'format': 'json', 'prop': 'imageinfo', 'titles': '|'.join(titles), - 'iiprop': 'timestamp|user|userid|comment|canonicaltitle|url|size|dimensions|sha1|mime|thumbmime|mediatype|bitdepth'} - return self.get_request_lc(**params) - - def get_backdrop(self, query): - data = self.get_images(self.get_titles(query)) - for k, v in data['query']['pages'].items(): - for i in v.get('imageinfo', []): - if i.get('width', 0) < 1280: - continue - if i.get('width', 0) < i.get('height', 0): - continue - if i.get('mime') != "image/jpeg": - continue - if i.get('url'): - return i.get('url') - - -class WikipediaAPI(RequestAPI): - def __init__(self, language=None): - lang = WIKI_LANGUAGE.get(language) or DEFAULT_WIKI_LANGUAGE - - self._wiki_tag_link = get_infolabel('Skin.String(Wikipedia.Format.Link)') or WIKI_TAG_LINK - self._wiki_tag_bold = get_infolabel('Skin.String(Wikipedia.Format.Bold)') or WIKI_TAG_BOLD - self._wiki_tag_emph = get_infolabel('Skin.String(Wikipedia.Format.Emphasis)') or WIKI_TAG_EMPH - self._wiki_tag_sups = get_infolabel('Skin.String(Wikipedia.Format.Superscript)') or WIKI_TAG_SUPS - - super(WikipediaAPI, self).__init__( - req_api_name='Wikipedia' if lang == DEFAULT_WIKI_LANGUAGE else f'Wikipedia_{lang}', - req_api_url=f'https://{lang}.wikipedia.org/w/api.php') - - def get_search(self, query, affix=None): - params = { - 'action': 'query', 'format': 'json', 'list': 'search', 'utf8': 1, - 'srsearch': f'{query} {affix}' if affix else query} - return self.get_request_lc(**params) - - def get_match(self, query, tmdb_type=None, match=''): - affixes = AFFIXES.get(tmdb_type, {}) - affix = affixes.get('affix') - _data = self.get_search(query, affix) - items = [i['title'] for i in _data['query']['search']] - x = xbmcgui.Dialog().select('Wikipedia', items) - if x == -1: - return - return items[x] - - def get_extract(self, title): - params = { - 'action': 'query', 'format': 'json', 'titles': title, 'prop': 'extracts', - 'exintro': True, 'explaintext': True} - return self.get_request_lc(**params) - - def get_sections(self, title): - params = { - 'action': 'parse', 'page': title, 'format': 'json', 'prop': 'sections', - 'disabletoc': True, 'redirects': ''} - try: - return self.get_request_lc(**params)['parse']['sections'] - except (KeyError, AttributeError, TypeError): - return [] - - def get_fullurl(self, title): - params = {'action': 'query', 'format': 'json', 'titles': title, 'prop': 'info', 'inprop': 'url'} - try: - data = self.get_request_lc(**params)['query']['pages'] - data = data[next(iter(data))]['fullurl'] - except (KeyError, AttributeError, TypeError): - return '' - return data - - def get_section(self, title, section_index): - params = { - 'action': 'parse', 'page': title, 'format': 'json', 'prop': 'text', - 'disabletoc': True, 'section': section_index, 'redirects': '', - 'disablelimitreport': True, - 'disableeditsection': True, - 'mobileformat': True} - return self.get_request_lc(**params) - - def get_all_sections(self, title): - sections = self.get_sections(title) - sections = [{'line': 'Overview', 'index': '0', 'number': '0'}] + sections - return sections - - def parse_links(self, data): - raw_html = data['parse']['text']['*'] - soup = BeautifulSoup(raw_html, 'html.parser') - links = [ - i['title'] for i in soup.find_all('a') - if i.get('title') - and i.get('href', '').startswith('/wiki/') - and not i['title'].startswith('Help:') - and not i['title'].startswith('Special:') - and not i['title'].startswith('Wikipedia:') - and not i['title'].startswith('Template:') - and not i['title'].startswith('Category:') - and not i.get('href', '').startswith('/wiki/File:')] - return links - - def parse_image(self, data): - raw_html = data['parse']['text']['*'] - soup = BeautifulSoup(raw_html, 'html.parser') - links = [i for i in soup.find_all('img') if i.get('src')] - return links - - def parse_text(self, data): - raw_html = data['parse']['text']['*'] - soup = BeautifulSoup(raw_html, 'html.parser') - text = [] - - def _parse_table(p): - for c in p.children: - if isinstance(c, Comment): - continue - if c.name in ['style']: - continue - if c.name and any(x in ['mw-references-wrap', 'references-text', 'mw-editsection'] for x in c.get('class', [])): - continue - if c.name in ['div', 'br']: - text.append(' ') - elif c.name in ['p', 'table', 'tr', 'li']: - text.append('\n\n') - if c.name == 'img' and c.get('title'): - text.append(f'{c["title"]}') - continue - if c.string: - if c.string.startswith('^'): - continue - t = c.string.replace('\n', ' ') - if c.name in ['th', 'td']: - t = f'{t} ' - if c.name and 'mw-headline' in c.get('class', ''): - t = self._wiki_tag_bold.format(t) - elif c.name in ['th', 'h2', 'b', 'h3', 'h1', 'h4']: - t = self._wiki_tag_bold.format(t) - elif c.name in ['i', 'em']: - t = self._wiki_tag_emph.format(t) - elif c.name in ['sup']: - t = self._wiki_tag_sups.format(t) - elif c.name in ['u', 'a']: - t = self._wiki_tag_link.format(t) - elif c.name in ['li']: - t = '* {}'.format(t) - text.append(f'{t}') - continue - if c.children: - _parse_table(c) - continue - - _parse_table(soup) - - text = ''.join(text) - text = re.sub(r'\[[0-9]*\]', '', text) - text = re.sub(r' +', ' ', text) - text = re.sub(r'( *\n){3,}', '\n\n', text) - text = re.sub(r'^(\n)+', '', text) - text = re.sub(r'^ +', '', text) - text = re.sub(r'\n +', '\n', text) - return text - - -class WikipediaGUI(xbmcgui.WindowXMLDialog): - def __init__(self, *args, **kwargs): - self._index = [] - self._query = kwargs.get('query') - self._tmdb_type = kwargs.get('tmdb_type') - self._wiki = WikipediaAPI(language=kwargs.get('language')) - self._wikimedia = WikimediaAPI() - self._backdrop = '' - self._title = '' - self._overview_img = '' - self._history = [] - with BusyDialog(): - self.do_setup() - - def do_setup(self, title=None): - self._title = title or self._wiki.get_match(self._query, self._tmdb_type) - if not self._title: - return - self._name = title or self._title - self._overview = self._wiki.parse_text(self._wiki.get_section(self._title, '0')) - self._sections = self._wiki.get_all_sections(self._title) - self._fullurl = self._wiki.get_fullurl(self._title) - - def do_init(self): - xbmcgui.Window(10000).clearProperty('Wikipedia.Backdrop') - self.clearProperty('Backdrop') - self._gui_name.setLabel(f'{self._title}') - self._gui_text.setText(f'{self._overview}') - self._gui_attr.setText(WIKI_ATTRIBUTION.format(self._fullurl)) - self._gui_ccim.setImage(WIKI_CCBYSA_IMG) - self.clearProperty('Image') - self.set_sections() - self.setFocusId(WIKI_LIST_ID) - self.set_section(0) - self._overview_img = self.get_image(0) - self._backdrop = self._wikimedia.get_backdrop(self._name) or '' - if self._backdrop: - xbmcgui.Window(10000).setProperty('Wikipedia.Backdrop', self._backdrop) - self.setProperty('Backdrop', self._backdrop) - - def onInit(self): - self._gui_name = self.getControl(WIKI_NAME_ID) - self._gui_list = self.getControl(WIKI_LIST_ID) - self._gui_text = self.getControl(WIKI_TEXT_ID) - self._gui_attr = self.getControl(WIKI_ATTR_ID) - self._gui_ccim = self.getControl(WIKI_CCIM_ID) - if not self._title: - self.close() - self.do_init() - - def onAction(self, action): - _action_id = action.getId() - if _action_id in ACTION_CLOSEWINDOW: - return self.do_close() - if _action_id in ACTION_MOVEMENT: - return self.do_scroll() - if _action_id in ACTION_SELECT: - return self.do_click() - - def do_close(self): - if self.getFocusId() == WIKI_SCRL_ID: - return self.setFocusId(WIKI_LIST_ID) - if not self._history: # No history so close - return self.close() - with BusyDialog(): # History so go back instead - self.do_setup(self._history.pop()) - self.do_init() - - def do_scroll(self): - if self.getFocusId() != WIKI_LIST_ID: - return - self.set_section(self._gui_list.getSelectedPosition()) - - def do_click(self): - if self.getFocusId() not in [WIKI_SCRL_ID, WIKI_LIST_ID]: - return - x = self._gui_list.getSelectedPosition() - links = self._wiki.parse_links(self._wiki.get_section(self._title, f'{x}')) - if not links: - return - links = list(dict.fromkeys(links)) - x = xbmcgui.Dialog().select('Links', links) - if x == -1: - return - self._history.append(self._title) - with BusyDialog(): - self.do_setup(links[x]) - self.do_init() - - def get_image(self, x): - try: - imgs = self._wiki.parse_image(self._wiki.get_section(self._title, f'{x}')) - except (TypeError, AttributeError, KeyError, IndexError): - return - if not imgs: - return - for img in imgs: - if int(img.get('width', 100)) < 32: - continue - if int(img.get('height', 100)) < 32: - continue - return img - - def set_image(self, img=None): - img = img or self._overview_img - self.setProperty('Image', f'https:{img.get("src")}') - self.setProperty('ImageText', f'{img.get("title") or img.get("alt")}') - - def set_section(self, x): - try: - text = self._index[x] - except (TypeError, AttributeError, KeyError, IndexError): - return - self._gui_text.setText(f'{text}') - self.set_image(self.get_image(x)) - - def set_sections(self): - self._index = [] - self._exit = False - itms = [] - for section in self._sections: - if self._exit: - break - name = section.get('line') - indx = section.get('index') - name = re.sub(r'<.*>', '', name) - numb = section.get('number') - name = f"{' ' if '.' in numb else ''}{numb} {name}" - if not name or not indx: - continue - itms.append((name, indx,)) - self._gui_list.reset() - self._gui_list.addItems([xbmcgui.ListItem(i) for i, j in itms]) - - def _threaditem(i): - indx = i[1] - text = self._wiki.parse_text(self._wiki.get_section(self._title, indx)) - text = text or WIKI_UNABLE_TO_PARSE_TEXT - return text - - with ParallelThread(itms, _threaditem) as pt: - item_queue = pt.queue - self._index = [i for i in item_queue] diff --git a/script.wikipedia/resources/lib/script.py b/script.wikipedia/resources/lib/script.py deleted file mode 100644 index a63abc1a1..000000000 --- a/script.wikipedia/resources/lib/script.py +++ /dev/null @@ -1,39 +0,0 @@ -from jurialmunkey.plugin import KodiPlugin -from jurialmunkey.parser import reconfigure_legacy_params -from lib.api import WikipediaGUI - - -KODIPLUGIN = KodiPlugin('script.wikipedia') -ADDONPATH = KODIPLUGIN._addon_path - - -def do_wikipedia_gui(wikipedia, tmdb_type=None, xml_file=None, language=None, **kwargs): - ui = WikipediaGUI( - xml_file or 'script-wikipedia.xml', ADDONPATH, 'default', '1080i', - query=wikipedia, tmdb_type=tmdb_type, language=language) - ui.doModal() - del ui - - -class Script(): - def __init__(self, *args): - self.params = {} - for arg in args: - if '=' in arg: - key, value = arg.split('=', 1) - self.params[key] = value.strip('\'').strip('"') if value else None - else: - self.params[arg] = True - self.params = reconfigure_legacy_params(**self.params) - - routing_table = { - 'wikipedia': - lambda **kwargs: do_wikipedia_gui(**kwargs)} - - def router(self): - if not self.params: - return - routes_available = set(self.routing_table.keys()) - params_given = set(self.params.keys()) - route_taken = set.intersection(routes_available, params_given).pop() - return self.routing_table[route_taken](**self.params) diff --git a/script.wikipedia/resources/script.py b/script.wikipedia/resources/script.py deleted file mode 100644 index 188526369..000000000 --- a/script.wikipedia/resources/script.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -# Module: default -# Author: jurialmunkey -# License: GPL v.3 https://www.gnu.org/copyleft/gpl.html -if __name__ == '__main__': - import sys - from lib.script import Script - Script(*sys.argv[1:]).router() diff --git a/script.wikipedia/resources/skins/default/1080i/script-wikipedia.xml b/script.wikipedia/resources/skins/default/1080i/script-wikipedia.xml deleted file mode 100644 index 87a37b7b9..000000000 --- a/script.wikipedia/resources/skins/default/1080i/script-wikipedia.xml +++ /dev/null @@ -1,132 +0,0 @@ - - - 9902 - - - 50% - 50% - 1000 - 1840 - - - - - - - - - 100 - - - - 0 - 80 - 520 - 2 - buttons/dialogbutton-nofo.png - - - 20 - 100 - 480 - 20 - 9902 - 9902 - 9001 - 61 - 200 - - - - - - - - - 500 - 80 - 345 - 2 - buttons/dialogbutton-nofo.png - - - 550 - 110 - 395 - 32 - black - font14 - Mono26 - 61 - - - 340 - 100 - 12 - 20 - 9902 - 9001 - vertical - - - - - 20 - 20 - 175 - 175 - keep - - - - - 20 - 80 - 300 - 61 - dialogbuttons_itemgap - 3 - - - - - - - - - - 20 - 180 - 285 - 180 - !String.IsEmpty(Window.Property(Image)) - - keep - $INFO[Window.Property(Image)] - 160 - - - center - center - 110 - 0 - font12 - bottom - grey - - - - - - - - - 20 - 80 - font12 - center - grey - - - - diff --git a/service.languagepreferencemanager/LICENSE.txt b/service.languagepreferencemanager/LICENSE.txt deleted file mode 100644 index f90922eea..000000000 --- a/service.languagepreferencemanager/LICENSE.txt +++ /dev/null @@ -1,340 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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 - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/service.languagepreferencemanager/README.md b/service.languagepreferencemanager/README.md deleted file mode 100644 index 5a14d4c8d..000000000 --- a/service.languagepreferencemanager/README.md +++ /dev/null @@ -1,30 +0,0 @@ -service.LanguagePreferenceManager -================================= - -A manager for audio and subtitle preferences -============================================ - -This addon provides an easy way to set your preferred audio streams and subtitle languages in Kodi. - -You can select which audio tracks and subtitles to automatically activate based on your priorities, and define simple conditional rules like "if audio is xxx then activate subtitles yyy" via drop/down lists. -More advanced custom rules can be defined as well (see changelog for more on the syntax. Note that custom rules always take precedence over others). - -Special language codes None(non) for subtitles and Any(any) for audio can be used in Conditional Subtitles Rules, normal or custom. -For example "fre:non>any:fre>any:eng" will disable subtitles if audio is French (except if a french forced subtitles track exists) and activate french subtitles for any other audio language. If these are not available it will try the same finding english subtitles. - -Rules are re-evaluated and applied whenever you switch audio while watching (from v0.1.5). - -It's now also possible to force ignore "Signs and Songs" subtitles in preferences evaluations, based on name, and/or any other subtitle tracks based on predefined keywords. - -For example, most dual audio Anime provides english and japanese audio and two english subtitles. Dialogue subtitles with all the dialogue to go with the japanese audio and Song/Sign subtitles which only translate song lyrics and signs you see on screen to be used with the english audio stream. Previously the addon just picked the first subtitles with the correct language which weren't always the correct ones. - -Special Thanks -============== - -- @ace20022 and @scott967 for initial development - -- @cyberden for making this addon ready for Kodi Matrix - -- @fpatrick for fixing an issue with language mapping - -- @KnappeGEIL for ideas how to ignore 'Signs and Songs' subtitles diff --git a/service.languagepreferencemanager/addon.xml b/service.languagepreferencemanager/addon.xml deleted file mode 100644 index 75a98295e..000000000 --- a/service.languagepreferencemanager/addon.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - executable - - - Language Preference Manager - Sets the audio and subtitle track according to your language preferences - For bugs, requests or general questions visit the Language Preference Manager thread on the Kodi forum. - all - 0.1.3 : Updated with changes required for Kodi 19 / Python 3 + Fixed Languages labels/tables, ConditionnalSubs to None, Custom settings -0.1.4BETA : Conditional Subtitles rules are re-assessed on-the-fly when toggling audio tracks -0.1.5 : Some minor optimisation -0.1.6 : Fix Custom CondSubs not working. Switch to ignore 'Signs' tracks. Add special 'Any' audio language code. Fix some typos -0.1.7BETA : New Keywords Blacklist to ignore chosen subtitles tracks based on name content. -1.0.0 : Mandatory clean-up to propose as candidate version for Kodi.tv addons repository. -1.0.1 : Initial version for main Kodi.tv addons repository -1.0.2 : Fix 10sec latency on subs display and possible few lines lost - at video start or when switching audio. - Forced sub tracks : check also subtitle field "isforced" in case field "name" is empty or misspelled. - - - - resources/icon.png - resources/fanart.jpg - - - diff --git a/service.languagepreferencemanager/changelog.txt b/service.languagepreferencemanager/changelog.txt deleted file mode 100644 index 3f16f6ec5..000000000 --- a/service.languagepreferencemanager/changelog.txt +++ /dev/null @@ -1,97 +0,0 @@ ---- Version 1.0.2 - -- Fix 10sec latency on subs display and possible few lines lost - at video start or when switching audio. -- Forced sub tracks : check also subtitle field "isforced" in case field "name" is empty or misspelled. - ---- Version 1.0.1 - -- Initial version for main Kodi.tv addons repository - ---- Version 1.0.0 - -- Mandatory clean-up to propose as candidate version for Kodi.tv addons repository. - ---- Version 0.1.7BETA - -- New Keywords Blacklist to ignore chosen subtitles tracks based on name content. - ---- Version 0.1.6 - -- Fix custom conditional preferences rules not working. (note : if ...>lan:non>... is used then forced tag is assumed on. See v0.1.3) -- Add a switch to completely ignore 'Signs and Songs' types of subtitle tracks. Default is off. -- Add special 'Any' audio language code to allow easy / simplified Conditional Subtitles rules like 'pol:non>any:pol'. -- Plus some small typo fixed. - ---- Version 0.1.5 - -- Fix preferences rules numbering in log messages. -- Skip extra genre/tag JSON video query when no custom prefs used at all. - ---- Version 0.1.4BETA - -- Conditional subtitle logic improvement. Rules are re-evaluated and applied on-the-fly if audio track is changed during the playback. - ---- Version 0.1.3 - -- Conditional subtitle logic adjustement : if "Lang1:None and forced tag set" then disable subtitles - but only if no Lang1 forced subtitle is found. Otherwise we activate this one. - ---- Version 0.1.2 - -- Adding an icon.png -- Fixing settings.xml CustomAudio, Sub and CondSub : allowempty=true to avoid multiple errors in the kodi log when not used. - ---- Version 0.1.0 - -- Fixing conditional subtitles like "French:None" not working. Not detected and then subtitles were not disabled. - (if sub_code is none) is the test to return -1 in default.py... But column 3 of None in langcodes.py is "non" as a string. - So I just changed the test in line 268 accordingly, and all is fine now. - ---- Version 0.0.7 + Matrix + Python3 + Languages fixed + more Languages fixed - -- Fix compatibility with matrix and Python3 -- Fix languages codes in string.po not aligned with KODI language codes (ex. French etc...) - ---- Version 0.0.6 - -- Fix compatibility with jarvis and krypton - ---- Version 0.0.5 - -- Added support for multiple 3 digit codes per language (thanks to waven!) - ---- Version 0.0.4 - -- Added a language called "None". If set as a conditinal subtitle language - subtitles get disabled if the audio language matches. - -- Added support for per genre/tag custom preferences. Syntax is - tag/genre1,tag/genre2...#(custom pref; see below)|tag/genre3,tag/genre4...#(custom pref; see below) - If no genre/tag# part is provided the pref gets evaluated for any genre/tag - ---- Version 0.0.3 - -- Custom preferences. Syntax is lang1>lang2>lang3>... for audio and sub - and lang1:lang2>lang3:lang4>... for cond. subs -- Wanilton's Portuguese (Brazil) translation, thanks! -- schumi2004's Dutch translation, thanks! - -[fixes] -subtitle preferences -use file name tracks regardless of other settings, if enabled - ---- Version 0.0.2 - -- A switch to enable subtitles if a preference matched. -- A switch to disable subtitles if no subtitle preference matched. -- The possibility to set the audio and/or subtitle track from the filename. - For example: matrix.audiostream_1.subtitle_2.avi - -[fixes] -conditional preferences -typos - ---- Version 0.0.1 - -First version of Language Preference Manager. - diff --git a/service.languagepreferencemanager/default.py b/service.languagepreferencemanager/default.py deleted file mode 100644 index 42fd32c06..000000000 --- a/service.languagepreferencemanager/default.py +++ /dev/null @@ -1,57 +0,0 @@ -import os, sys, re -import xbmc, xbmcaddon, xbmcvfs - -import json as simplejson - -__addon__ = xbmcaddon.Addon() -__addonversion__ = __addon__.getAddonInfo('version') -__addonid__ = __addon__.getAddonInfo('id') -__addonname__ = __addon__.getAddonInfo('name') -__addonPath__ = __addon__.getAddonInfo('path') -__addonResourcePath__ = xbmcvfs.translatePath(os.path.join(__addonPath__, 'resources', 'lib')) -__addonIconFile__ = xbmcvfs.translatePath(os.path.join(__addonPath__, 'icon.png')) -sys.path.append(__addonResourcePath__) - -from langcodes import * -from prefsettings import settings -from prefutils import LangPref_Monitor -from prefutils import LangPrefMan_Player - -settings = settings() - -LOG_NONE = 0 -LOG_ERROR = 1 -LOG_INFO = 2 -LOG_DEBUG = 3 - -def log(level, msg): - if level <= settings.logLevel: - if level == LOG_ERROR: - l = xbmc.LOGERROR - elif level == LOG_INFO: - l = xbmc.LOGINFO - elif level == LOG_DEBUG: - l = xbmc.LOGDEBUG - xbmc.log("[Language Preference Manager]: " + str(msg), l) - -class Main: - def __init__( self ): - self._init_vars() - if (not settings.service_enabled): - log(LOG_INFO, "Service not enabled") - - settings.readSettings() - self._daemon() - - def _init_vars( self ): - self.Monitor = LangPref_Monitor() - self.Player = LangPrefMan_Player() - - def _daemon( self ): - while (not self.Monitor.abortRequested()): - self.Monitor.waitForAbort(1) - -if ( __name__ == "__main__" ): - log(LOG_INFO, 'service {0} version {1} started'.format(__addonname__, __addonversion__)) - main = Main() - log(LOG_INFO, 'service {0} version {1} stopped'.format(__addonname__, __addonversion__)) diff --git a/service.languagepreferencemanager/resources/fanart.jpg b/service.languagepreferencemanager/resources/fanart.jpg deleted file mode 100644 index e7883991d..000000000 Binary files a/service.languagepreferencemanager/resources/fanart.jpg and /dev/null differ diff --git a/service.languagepreferencemanager/resources/icon.png b/service.languagepreferencemanager/resources/icon.png deleted file mode 100644 index 6c03207a9..000000000 Binary files a/service.languagepreferencemanager/resources/icon.png and /dev/null differ diff --git a/service.languagepreferencemanager/resources/language/resource.language.en_gb/strings.po b/service.languagepreferencemanager/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index 252509de9..000000000 --- a/service.languagepreferencemanager/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,314 +0,0 @@ -# Kodi Media Center language file -# Addon Name: Language Preference Manager -# Addon id: service.languagepreferencemanager -# Addon Provider: ace20022 -# Addon Updater: PP to fix languages codes not correct in string.po and langcodes.py, and not aligned -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: https://forum.kodi.tv/\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Kodi Translation Team\n" -"Language-Team: Team-Kodi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en_GB\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -msgctxt "#30100" -msgid "Languages" -msgstr "" - -msgctxt "#30101" -msgid "Primary Language" -msgstr "" - -msgctxt "#30102" -msgid "Secondary Language" -msgstr "" - -msgctxt "#30103" -msgid "Third Language" -msgstr "" - -msgctxt "#30104" -msgid "Audio Preferences" -msgstr "" - -msgctxt "#30105" -msgid "Subtitle Preferences" -msgstr "" - -msgctxt "#30106" -msgid "Conditional Subtitle Preferences" -msgstr "" - -msgctxt "#30107" -msgid "Enable Preference" -msgstr "" - -msgctxt "#30108" -msgid "Enable Language Preference Manager" -msgstr "" - -msgctxt "#30109" -msgid "Pause while evaluating preferences" -msgstr "" - -msgctxt "#30110" -msgid "If audio language is " -msgstr "" - -msgctxt "#30111" -msgid "then display subtitles with language" -msgstr "" - -msgctxt "#30112" -msgid "Delay the evaluation by the following value [ms]" -msgstr "" - -msgctxt "#30113" -msgid "Turn on subtitles if a subtitle prefrence matched" -msgstr "" - -msgctxt "#30114" -msgid "Turn off subtitles if no subtitle prefrence matched" -msgstr "" - -msgctxt "#30115" -msgid "Advanced/Custom settings" -msgstr "" - -msgctxt "#30116" -msgid "Try to set the audio and/or subtitle track from the filename first" -msgstr "" - -msgctxt "#30117" -msgid "Regular expression for filename based preferences" -msgstr "" - -msgctxt "#30118" -msgid "Use custom pref, e.g. eng>ger>fin :" -msgstr "" - -msgctxt "#30119" -msgid "Use custom pref, e.g. eng>ger>fin :" -msgstr "" - -msgctxt "#30120" -msgid "Forced" -msgstr "" - -msgctxt "#30121" -msgid "Use custom pref, e.g. jpn:eng>eng:ger :" -msgstr "" - -msgctxt "#30122" -msgid "General" -msgstr "" - -msgctxt "#30123" -msgid "Ignore all Signs and Songs subtitles tracks" -msgstr "" - -msgctxt "#30125" -msgid "Enable Keyword Blacklist" -msgstr " - -msgctxt "#30126" -msgid "Enter a comma-separated list of keywords to blacklist in subtitle names" -msgstr " - -msgctxt "#30127" -msgid "Keyword List:" -msgstr " - -msgctxt "#30201" -msgid "Albanian" -msgstr "" - -msgctxt "#30202" -msgid "Arabic" -msgstr "" - -msgctxt "#30203" -msgid "Belarusian" -msgstr "" - -msgctxt "#30204" -msgid "Bosnian (Latin)" -msgstr "" - -msgctxt "#30205" -msgid "Bulgarian" -msgstr "" - -msgctxt "#30206" -msgid "Catalan" -msgstr "" - -msgctxt "#30207" -msgid "Chinese" -msgstr "" - -msgctxt "#30208" -msgid "Croatian" -msgstr "" - -msgctxt "#30209" -msgid "Czech" -msgstr "" - -msgctxt "#30210" -msgid "Danish" -msgstr "" - -msgctxt "#30211" -msgid "Dutch" -msgstr "" - -msgctxt "#30212" -msgid "English" -msgstr "" - -msgctxt "#30213" -msgid "Estonian" -msgstr "" - -msgctxt "#30214" -msgid "Farsi" -msgstr "" - -msgctxt "#30215" -msgid "Finnish" -msgstr "" - -msgctxt "#30216" -msgid "French" -msgstr "" - -msgctxt "#30217" -msgid "German" -msgstr "" - -msgctxt "#30218" -msgid "Greek" -msgstr "" - -msgctxt "#30219" -msgid "Hebrew" -msgstr "" - -msgctxt "#30220" -msgid "Hindi" -msgstr "" - -msgctxt "#30221" -msgid "Hungarian" -msgstr "" - -msgctxt "#30222" -msgid "Icelandic" -msgstr "" - -msgctxt "#30223" -msgid "Indonesian" -msgstr "" - -msgctxt "#30224" -msgid "Italian" -msgstr "" - -msgctxt "#30225" -msgid "Japanese" -msgstr "" - -msgctxt "#30226" -msgid "Korean" -msgstr "" - -msgctxt "#30227" -msgid "Latvian" -msgstr "" - -msgctxt "#30228" -msgid "Lithuanian" -msgstr "" - -msgctxt "#30229" -msgid "Macedonian" -msgstr "" - -msgctxt "#30230" -msgid "Norwegian" -msgstr "" - -msgctxt "#30231" -msgid "Persian" -msgstr "" - -msgctxt "#30232" -msgid "Polish" -msgstr "" - -msgctxt "#30233" -msgid "Portuguese" -msgstr "" - -msgctxt "#30234" -msgid "Portuguese (Brazil)" -msgstr "" - -msgctxt "#30235" -msgid "Romanian" -msgstr "" - -msgctxt "#30236" -msgid "Russian" -msgstr "" - -msgctxt "#30237" -msgid "Serbian" -msgstr "" - -msgctxt "#30238" -msgid "Slovak" -msgstr "" - -msgctxt "#30239" -msgid "Slovenian" -msgstr "" - -msgctxt "#30240" -msgid "Spanish" -msgstr "" - -msgctxt "#30241" -msgid "Swedish" -msgstr "" - -msgctxt "#30242" -msgid "Thai" -msgstr "" - -msgctxt "#30243" -msgid "Turkish" -msgstr "" - -msgctxt "#30244" -msgid "Ukrainian" -msgstr "" - -msgctxt "#30245" -msgid "Vietnamese" -msgstr "" - -msgctxt "#30200" -msgid "None" -msgstr "" - -msgctxt "#30300" -msgid "Any" -msgstr "" diff --git a/service.languagepreferencemanager/resources/lib/langcodes.py b/service.languagepreferencemanager/resources/lib/langcodes.py deleted file mode 100644 index 6f4e392aa..000000000 --- a/service.languagepreferencemanager/resources/lib/langcodes.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- - -# This script is based on the one of script.xbmc.subtitles -# Thanks to their original authors amet, mr_blobby - - - -LANGUAGES = ( - - # Full Language name[0] podnapisi[1] ISO 639-1[2] ISO 639-1 Code[3] Script Setting Language[4] localized name id number[5] - - ("Albanian" , "29", "sq", "alb", "0", 30201 ), - ("Arabic" , "12", "ar", "ara", "1", 30202 ), - ("Belarusian" , "0" , "hy", "arm", "2", 30203 ), - ("Bosnian" , "10", "bs", "bos", "3", 30204 ), - ("Bulgarian" , "33", "bg", "bul", "4", 30205 ), - ("Catalan" , "53", "ca", "cat", "5", 30206 ), - ("Chinese" , "17", "zh", "chi", "6", 30207 ), - ("Croatian" , "38", "hr", "hrv", "7", 30208 ), - ("Czech" , "7", "cs", "cze", "8", 30209 ), - ("Danish" , "24", "da", "dan", "9", 30210 ), - ("Dutch" , "23", "nl", "dut", "10", 30211 ), - ("English" , "2", "en", "eng", "11", 30212 ), - ("Estonian" , "20", "et", "est", "12", 30213 ), - ("Farsi" , "52", "fa", "per", "13", 30214 ), - ("Finnish" , "31", "fi", "fin", "14", 30215 ), - ("French" , "8", "fr", "fre", "15", 30216 ), - ("German" , "5", "de", "ger,deu", "16", 30217 ), - ("Greek" , "16", "el", "ell", "17", 30218 ), - ("Hebrew" , "22", "he", "heb", "18", 30219 ), - ("Hindi" , "42", "hi", "hin", "19", 30220 ), - ("Hungarian" , "15", "hu", "hun", "20", 30221 ), - ("Icelandic" , "6", "is", "ice", "21", 30222 ), - ("Indonesian" , "0", "id", "ind", "22", 30223 ), - ("Italian" , "9", "it", "ita", "23", 30224 ), - ("Japanese" , "11", "ja", "jpn", "24", 30225 ), - ("Korean" , "4", "ko", "kor", "25", 30226 ), - ("Latvian" , "21", "lv", "lav", "26", 30227 ), - ("Lithuanian" , "0", "lt", "lit", "27", 30228 ), - ("Macedonian" , "35", "mk", "mac", "28", 30229 ), - ("Norwegian" , "3", "no", "nor", "29", 30230 ), - ("Persian" , "52", "fa", "per", "30", 30231 ), - ("Polish" , "26", "pl", "pol", "31", 30232 ), - ("Portuguese" , "32", "pt", "por", "32", 30233 ), - ("Portuguese (Brazil)" , "48", "pb", "pt-br", "33", 30234 ), - ("Romanian" , "13", "ro", "rum", "34", 30235 ), - ("Russian" , "27", "ru", "rus", "35", 30236 ), - ("Serbian" , "36", "sr", "scc", "36", 30237 ), - ("Slovak" , "37", "sk", "slo", "37", 30238 ), - ("Slovenian" , "1", "sl", "slv", "38", 30239 ), - ("Spanish" , "28", "es", "spa", "39", 30240 ), - ("Swedish" , "25", "sv", "swe", "40", 30241 ), - ("Thai" , "0", "th", "tha", "41", 30242 ), - ("Turkish" , "30", "tr", "tur", "42", 30243 ), - ("Ukrainian" , "46", "uk", "ukr", "43", 30244 ), - ("Vietnamese" , "51", "vi", "vie", "44", 30245 ), - ("None" , "-1", "", "non", "45", 30200 ), - ("Any" , "-2", "", "any", "46", 30300 ) ) - -def languageTranslate(lang, lang_from, lang_to): - for x in LANGUAGES: - codes = x[lang_from].split(r',') - for code in codes: - if lang == code : - return x[lang_to] diff --git a/service.languagepreferencemanager/resources/lib/prefparser.py b/service.languagepreferencemanager/resources/lib/prefparser.py deleted file mode 100644 index 4fa3b01a1..000000000 --- a/service.languagepreferencemanager/resources/lib/prefparser.py +++ /dev/null @@ -1,100 +0,0 @@ -import re -import xbmc, xbmcaddon -from langcodes import * - -LOG_NONE = 0 -LOG_ERROR = 1 -LOG_INFO = 2 -LOG_DEBUG = 3 - -class PrefParser: - - def log(self, level, msg): - if level <= self.logLevel: - if level == LOG_ERROR: - l = xbmc.LOGERROR - elif level == LOG_INFO: - l = xbmc.LOGINFO - elif level == LOG_DEBUG: - l = xbmc.LOGDEBUG - xbmc.log("[Language Preference Manager]: " + str(msg), l) - - def __init__( self ): - addon = xbmcaddon.Addon() - self.logLevel = addon.getSetting('log_level') - if self.logLevel and len(self.logLevel) > 0: - self.logLevel = int(self.logLevel) - else: - self.logLevel = LOG_INFO - self.custom_prefs_delim = r'>' - self.custom_genre_prefs_delim = r'|' - self.custom_g_t_pref_delim = r'#' - self.custom_g_t_delim = r',' - self.custom_condSub_delim = r':' - - def parsePrefString(self, pref_string): - preferences = [] - if not pref_string: - return preferences - - if (pref_string.find(self.custom_genre_prefs_delim) > 0): - c_prefs = pref_string.split(self.custom_genre_prefs_delim) - else: - c_prefs = [pref_string] - - for s_pref in c_prefs: - pref = self.parseSinglePref(s_pref) - if (pref): - preferences.append(pref) - - if (len(preferences) == 1 - and isinstance(preferences[0], list)): - preferences = preferences[0] - - return preferences - - def parseSinglePref(self, s_pref): - if (s_pref.find(self.custom_g_t_pref_delim) > 0): - g_pref = s_pref.split(self.custom_g_t_pref_delim) - if len(g_pref ) != 2: - self.log(LOG_INFO, 'Parse error: {0}'.format(g_pref)) - return [] - else: - return (set(map(lambda x:x.lower(), g_pref[0].split(self.custom_g_t_delim))), - self.parsePref(g_pref[1])) - else: - return (set(), self.parsePref(s_pref)) - - def parsePref(self, prefs): - lang_prefs = [] - if (prefs.find(self.custom_prefs_delim) > 0): - s_prefs = prefs.split(self.custom_prefs_delim) - else: - s_prefs = [prefs] - for pref in s_prefs: - # custom cond sub pref - if (pref.find(self.custom_condSub_delim) > 0): - pref = pref.split(self.custom_condSub_delim) - if len(pref) != 2: - self.log(LOG_INFO, 'Custom cond subs prefs parse error: {0}'.format(pref)) - else: - temp_a = (languageTranslate(pref[0], 3, 0), pref[0]) - temp_s = (languageTranslate(pref[1], 3, 0), pref[1]) - if (temp_a[0] and temp_a[1] and temp_s[0] and temp_s[1]): - if (temp_s[1] == 'non'): - forced_tag = 'true' - else: - forced_tag = 'false' - lang_prefs.append((temp_a[0], temp_a[1], temp_s[0], temp_s[1], forced_tag)) - else: - self.log(LOG_INFO, 'Custom cond sub prefs: lang code not found in db!'\ - ' Please report this: {0}:{1}'.format(temp_a, temp_s)) - # custom audio or subtitle pref - else: - temp_pref = (languageTranslate(pref, 3, 0), pref) - if temp_pref[0]: - lang_prefs.append(temp_pref) - else: - self.log(LOG_INFO, 'Custom audio prefs: lang code {0} not found in db!'\ - ' Please report this'.format(pref)) - return lang_prefs diff --git a/service.languagepreferencemanager/resources/lib/prefsettings.py b/service.languagepreferencemanager/resources/lib/prefsettings.py deleted file mode 100644 index 43476518d..000000000 --- a/service.languagepreferencemanager/resources/lib/prefsettings.py +++ /dev/null @@ -1,164 +0,0 @@ -import xbmc, xbmcaddon -import re -from langcodes import * -from prefparser import PrefParser - -LOG_NONE = 0 -LOG_ERROR = 1 -LOG_INFO = 2 -LOG_DEBUG = 3 - - - -class settings(): - - def log(self, level, msg): - if level <= self.logLevel: - if level == LOG_ERROR: - l = xbmc.LOGERROR - elif level == LOG_INFO: - l = xbmc.LOGINFO - elif level == LOG_DEBUG: - l = xbmc.LOGDEBUG - xbmc.log("[Language Preference Manager]: " + str(msg), l) - - def init(self): - addon = xbmcaddon.Addon() - self.logLevel = addon.getSetting('log_level') - if self.logLevel and len(self.logLevel) > 0: - self.logLevel = int(self.logLevel) - else: - self.logLevel = LOG_INFO - - self.custom_audio = [] - self.custom_subs = [] - self.custom_condsub = [] - - self.service_enabled = addon.getSetting('enabled') == 'true' - - def __init__( self ): - self.init() - - def readSettings(self): - self.readPrefs() - self.readCustomPrefs() - self.log(LOG_DEBUG, - '\n##### LPM Settings #####\n' \ - 'delay: {0}ms\n' \ - 'audio on: {1}\n' \ - 'subs on: {2}\n' \ - 'cond subs on: {3}\n' \ - 'turn subs on: {4}, turn subs off: {5}\n' \ - 'signs: {15}\n' \ - 'blacklisted keywords: {16}\n' \ - 'use file name: {6}, file name regex: {7}\n' \ - 'at least one pref on: {8}\n'\ - 'audio prefs: {9}\n' \ - 'sub prefs: {10}\n' \ - 'cond sub prefs: {11}\n' \ - 'custom audio prefs: {12}\n' \ - 'custom subs prefs: {13}\n' - 'custom cond subs prefs: {14}\n' - '##### LPM Settings #####\n' - .format(self.delay, self.audio_prefs_on, self.sub_prefs_on, - self.condsub_prefs_on, self.turn_subs_on, self.turn_subs_off, - self.useFilename, self.filenameRegex, self.at_least_one_pref_on, - self.AudioPrefs, self.SubtitlePrefs, self.CondSubtitlePrefs, - self.custom_audio, self.custom_subs, self.custom_condsub, self.ignore_signs_on, - ','.join(self.keyword_blacklist)) - ) - - def readPrefs(self): - addon = xbmcaddon.Addon() - - self.service_enabled = addon.getSetting('enabled') == 'true' - self.delay = int(addon.getSetting('delay')) - self.audio_prefs_on = addon.getSetting('enableAudio') == 'true' - self.sub_prefs_on = addon.getSetting('enableSub') == 'true' - self.condsub_prefs_on = addon.getSetting('enableCondSub') == 'true' - self.turn_subs_on = addon.getSetting('turnSubsOn') == 'true' - self.turn_subs_off = addon.getSetting('turnSubsOff') == 'true' - self.ignore_signs_on = addon.getSetting('signs') == 'true' - self.keyword_blacklist_enabled = addon.getSetting('enableKeywordBlacklist') == 'true' - self.keyword_blacklist = addon.getSetting('KeywordBlacklist') - if self.keyword_blacklist and self.keyword_blacklist_enabled: - self.keyword_blacklist = self.keyword_blacklist.lower().split(',') - else: - self.keyword_blacklist = [] - self.useFilename = addon.getSetting('useFilename') == 'true' - self.filenameRegex = addon.getSetting('filenameRegex') - if self.useFilename: - self.reg = re.compile(self.filenameRegex, re.IGNORECASE) - self.split = re.compile(r'[_|.|-]*', re.IGNORECASE) - - self.at_least_one_pref_on = (self.audio_prefs_on - or self.sub_prefs_on - or self.condsub_prefs_on - or self.useFilename) - - self.AudioPrefs = [(set(), [ - (languageTranslate(addon.getSetting('AudioLang01'), 4, 0) , - languageTranslate(addon.getSetting('AudioLang01'), 4, 3)), - (languageTranslate(addon.getSetting('AudioLang02'), 4, 0) , - languageTranslate(addon.getSetting('AudioLang02'), 4, 3)), - (languageTranslate(addon.getSetting('AudioLang03'), 4, 0) , - languageTranslate(addon.getSetting('AudioLang03'), 4, 3))] - )] - self.SubtitlePrefs = [(set(), [ - (languageTranslate(addon.getSetting('SubLang01'), 4, 0) , - languageTranslate(addon.getSetting('SubLang01'), 4, 3), - addon.getSetting('SubForced01')), - (languageTranslate(addon.getSetting('SubLang02'), 4, 0) , - languageTranslate(addon.getSetting('SubLang02'), 4, 3), - addon.getSetting('SubForced02')), - (languageTranslate(addon.getSetting('SubLang03'), 4, 0) , - languageTranslate(addon.getSetting('SubLang03'), 4, 3), - addon.getSetting('SubForced03'))] - )] - self.CondSubtitlePrefs = [(set(), [ - ( - languageTranslate(addon.getSetting('CondAudioLang01'), 4, 0), - languageTranslate(addon.getSetting('CondAudioLang01'), 4, 3), - languageTranslate(addon.getSetting('CondSubLang01'), 4, 0), - languageTranslate(addon.getSetting('CondSubLang01'), 4, 3), - addon.getSetting('CondSubForced01') - ), - ( - languageTranslate(addon.getSetting('CondAudioLang02'), 4, 0), - languageTranslate(addon.getSetting('CondAudioLang02'), 4, 3), - languageTranslate(addon.getSetting('CondSubLang02'), 4, 0), - languageTranslate(addon.getSetting('CondSubLang02'), 4, 3), - addon.getSetting('CondSubForced02') - ), - ( - languageTranslate(addon.getSetting('CondAudioLang03'), 4, 0), - languageTranslate(addon.getSetting('CondAudioLang03'), 4, 3), - languageTranslate(addon.getSetting('CondSubLang03'), 4, 0), - languageTranslate(addon.getSetting('CondSubLang03'), 4, 3), - addon.getSetting('CondSubForced03') - )] - )] - - def readCustomPrefs(self): - addon = xbmcaddon.Addon() - self.custom_audio = [] - self.custom_audio_prefs_on = False - self.custom_subs = [] - self.custom_sub_prefs_on = False - self.custom_condsub = [] - self.custom_condsub_prefs_on = False - - prefParser = PrefParser() - self.custom_audio = prefParser.parsePrefString( - addon.getSetting('CustomAudio')) - self.custom_subs = prefParser.parsePrefString( - addon.getSetting('CustomSub')) - self.custom_condsub = prefParser.parsePrefString( - addon.getSetting('CustomCondSub')) - - if len(self.custom_audio) > 0: - self.custom_audio_prefs_on = True - if len(self.custom_subs) > 0: - self.custom_sub_prefs_on = True - if len(self.custom_condsub) >0: - self.custom_condsub_prefs_on = True diff --git a/service.languagepreferencemanager/resources/lib/prefutils.py b/service.languagepreferencemanager/resources/lib/prefutils.py deleted file mode 100644 index 7d9b9e994..000000000 --- a/service.languagepreferencemanager/resources/lib/prefutils.py +++ /dev/null @@ -1,382 +0,0 @@ -import os, sys, re -import xbmc, xbmcaddon, xbmcvfs - -import json as simplejson - -from langcodes import * -from prefsettings import settings - -settings = settings() - -LOG_NONE = 0 -LOG_ERROR = 1 -LOG_INFO = 2 -LOG_DEBUG = 3 - -def log(level, msg): - if level <= settings.logLevel: - if level == LOG_ERROR: - l = xbmc.LOGERROR - elif level == LOG_INFO: - l = xbmc.LOGINFO - elif level == LOG_DEBUG: - l = xbmc.LOGDEBUG - xbmc.log("[Language Preference Manager]: " + str(msg), l) - -class LangPref_Monitor( xbmc.Monitor ): - - def __init__( self ): - xbmc.Monitor.__init__( self ) - - def onSettingsChanged( self ): - settings.init() - settings.readSettings() - -class LangPrefMan_Player(xbmc.Player) : - - def __init__ (self): - self.LPM_initial_run_done = False - settings.readSettings() - xbmc.Player.__init__(self) - - def onPlayBackStarted(self): - if settings.service_enabled and settings.at_least_one_pref_on: - log(LOG_DEBUG, 'New AV Playback initiated - Resetting LPM Initial Flag') - self.LPM_initial_run_done = False - - def onAVStarted(self): - if settings.service_enabled and settings.at_least_one_pref_on and self.isPlayingVideo(): - log(LOG_DEBUG, 'Playback started') - self.audio_changed = False - # switching an audio track to early leads to a reopen -> start at the beginning - if settings.delay > 0: - log(LOG_DEBUG, "Delaying preferences evaluation by {0} ms".format(settings.delay)) - xbmc.sleep(settings.delay) - log(LOG_DEBUG, 'Getting video properties') - self.getDetails() - self.evalPrefs() - self.LPM_initial_run_done = True - - def onAVChange(self): - if self.LPM_initial_run_done and settings.service_enabled and settings.at_least_one_pref_on and self.isPlayingVideo(): - log(LOG_DEBUG, 'AVChange detected - Checking possible change of audio track...') - self.audio_changed = False - if settings.delay > 0: - log(LOG_DEBUG, "Delaying preferences evaluation by {0} ms".format(settings.delay)) - xbmc.sleep(settings.delay) - previous_audio_index = self.selected_audio_stream['index'] - previous_audio_language = self.selected_audio_stream['language'] - log(LOG_DEBUG, 'Getting video properties') - self.getDetails() - if (self.selected_audio_stream['index'] != previous_audio_index): - log(LOG_INFO, 'Audio track changed from {0} to {1}. Reviewing Conditional Subtitles rules...'.format(previous_audio_language, self.selected_audio_stream['language'])) - self.evalPrefs() - - def evalPrefs(self): - # recognized filename audio or filename subtitle - fa = False - fs = False - if settings.useFilename and not self.LPM_initial_run_done: - audio, sub = self.evalFilenamePrefs() - if (audio >= 0) and audio < len(self.audiostreams): - log(LOG_INFO, 'Filename preference: Match, selecting audio track {0}'.format(audio)) - self.setAudioStream(audio) - self.audio_changed = True - fa = True - else: - log(LOG_INFO, 'Filename preference: No match found for audio track ({0})'.format(self.getPlayingFile())) - - if (sub >= 0) and sub < len(self.subtitles): - self.setSubtitleStream(sub) - fs = True - log(LOG_INFO, 'Filename preference: Match, selecting subtitle track {0}'.format(sub)) - if settings.turn_subs_on: - log(LOG_DEBUG, 'Subtitle: enabling subs' ) - self.showSubtitles(True) - else: - log(LOG_INFO, 'Filename preference: No match found for subtitle track ({0})'.format(self.getPlayingFile())) - if settings.turn_subs_off: - log(LOG_INFO, 'Subtitle: disabling subs' ) - self.showSubtitles(False) - - if settings.audio_prefs_on and not fa and not self.LPM_initial_run_done: - if settings.custom_audio_prefs_on: - trackIndex = self.evalAudioPrefs(settings.custom_audio) - else: - trackIndex = self.evalAudioPrefs(settings.AudioPrefs) - - if trackIndex == -2: - log(LOG_INFO, 'Audio: None of the preferred languages is available' ) - elif trackIndex >= 0: - self.setAudioStream(trackIndex) - self.audio_changed = True - - if settings.sub_prefs_on and not fs and not self.LPM_initial_run_done: - if settings.custom_sub_prefs_on: - trackIndex = self.evalSubPrefs(settings.custom_subs) - else: - trackIndex = self.evalSubPrefs(settings.SubtitlePrefs) - - if trackIndex == -2: - log(LOG_INFO, 'Subtitle: None of the preferred languages is available' ) - if settings.turn_subs_off: - log(LOG_INFO, 'Subtitle: disabling subs' ) - self.showSubtitles(False) - if trackIndex == -1: - log(LOG_INFO, 'Subtitle: Preferred subtitle is selected but might not be enabled' ) - if settings.turn_subs_on and not self.selected_sub_enabled: - log(LOG_INFO, 'Subtitle: enabling subs because selected sub is not enabled' ) - self.showSubtitles(True) - elif trackIndex >= 0: - self.setSubtitleStream(trackIndex) - if settings.turn_subs_on: - log(LOG_INFO, 'Subtitle: enabling subs' ) - self.showSubtitles(True) - - if settings.condsub_prefs_on and not fs: - if settings.custom_condsub_prefs_on: - trackIndex = self.evalCondSubPrefs(settings.custom_condsub) - else: - trackIndex = self.evalCondSubPrefs(settings.CondSubtitlePrefs) - - if trackIndex == -1: - log(LOG_INFO, 'Conditional subtitle: disabling subs' ) - self.showSubtitles(False) - if trackIndex == -2: - log(LOG_INFO, 'Conditional subtitle: No matching preferences found for current audio stream. Doing nothing.') - elif trackIndex >= 0: - self.setSubtitleStream(trackIndex) - if settings.turn_subs_on: - log(LOG_DEBUG, 'Subtitle: enabling subs' ) - self.showSubtitles(True) - - # Workaround to an old Kodi bug creating 10-15 sec latency when activating a subtitle track. - # Force very short rewind to avoid 10-15sec delay and first few subtitles lines potentially lost - # but if we are very close to beginning, then restart from time 0 - if (self.getTime() <= 10): - self.seekTime(0) - else: - self.seekTime(self.getTime()-1) - - def evalFilenamePrefs(self): - log(LOG_DEBUG, 'Evaluating filename preferences' ) - audio = -1 - sub = -1 - filename = self.getPlayingFile() - matches = settings.reg.findall(filename) - fileprefs = [] - for m in matches: - sp = settings.split.split(m) - fileprefs.append(sp) - - for pref in fileprefs: - if len(pref) == 2: - if (pref[0].lower() == 'audiostream'): - audio = int(pref[1]) - log(LOG_INFO, 'audio track extracted from filename: {0}'.format(audio)) - elif(pref[0].lower() == 'subtitle'): - sub = int(pref[1]) - log(LOG_INFO, 'subtitle track extracted from filename: {0}'.format(sub)) - log(LOG_DEBUG, 'filename: audio: {0}, sub: {1} ({2})'.format(audio, sub, filename)) - return audio, sub - - def evalAudioPrefs(self, audio_prefs): - log(LOG_DEBUG, 'Evaluating audio preferences' ) - i = 0 - for pref in audio_prefs: - i += 1 - g_t, preferences = pref - # genre or tags are given (g_t not empty) but none of them matches the video's tags/genres - if g_t and (not (self.genres_and_tags & g_t)): - continue - - log(LOG_INFO,'Audio: genre/tag preference {0} met with intersection {1}'.format(g_t, (self.genres_and_tags & g_t))) - for pref in preferences: - name, codes = pref - codes = codes.split(r',') - for code in codes: - if (code is None): - log(LOG_DEBUG,'continue') - continue - if (self.selected_audio_stream and - 'language' in self.selected_audio_stream and - (code == self.selected_audio_stream['language'] or name == self.selected_audio_stream['language'])): - log(LOG_INFO, 'Selected audio language matches preference {0} ({1})'.format(i, name) ) - return -1 - else: - for stream in self.audiostreams: - if ((code == stream['language']) or (name == stream['language'])): - log(LOG_INFO, 'Audio language of stream {0} matches preference {1} ({2})'.format(stream['index'], i, name) ) - return stream['index'] - log(LOG_INFO, 'Audio: preference {0} ({1}:{2}) not available'.format(i, name, code) ) - i += 1 - return -2 - - def evalSubPrefs(self, sub_prefs): - log(LOG_DEBUG, 'Evaluating subtitle preferences' ) - log(LOG_DEBUG, 'Subtitle names containing the following keywords are blacklisted: {0}'.format(','.join(settings.keyword_blacklist))) - i = 0 - for pref in sub_prefs: - i += 1 - g_t, preferences = pref - # genre or tags are given (g_t not empty) but none of them matches the video's tags/genres - if g_t and (not (self.genres_and_tags & g_t)): - continue - - log(LOG_INFO,'Subtitle: genre/tag preference {0} met with intersection {1}'.format(g_t, (self.genres_and_tags & g_t))) - for pref in preferences: - name, codes, forced = pref - codes = codes.split(r',') - for code in codes: - if (code is None): - log(LOG_DEBUG,'continue') - continue - if (self.selected_sub and - 'language' in self.selected_sub and - ((code == self.selected_sub['language'] or name == self.selected_sub['language']) and self.testForcedFlag(forced, self.selected_sub['name'], self.selected_sub['isforced']))): - log(LOG_INFO, 'Selected subtitle language matches preference {0} ({1})'.format(i, name) ) - return -1 - else: - for sub in self.subtitles: - if (settings.keyword_blacklist_enabled and any(keyword in sub['name'].lower() for keyword in settings.keyword_blacklist)): - log(LOG_INFO,'SubPrefs : one subtitle track is found matching Keyword Blacklist : {0}. Skipping it.'.format(','.join(settings.keyword_blacklist))) - continue - if (settings.ignore_signs_on and self.isSignsSub(sub['name'])): - log(LOG_INFO,'SubPrefs : ignore_signs toggle is on and one such subtitle track is found. Skipping it.') - continue - if ((code == sub['language'] or name == sub['language']) and self.testForcedFlag(forced, sub['name'], sub['isforced'])): - log(LOG_INFO, 'Subtitle language of subtitle {0} matches preference {1} ({2})'.format((sub['index']+1), i, name) ) - return sub['index'] - log(LOG_INFO, 'Subtitle: preference {0} ({1}:{2}) not available'.format(i, name, code) ) - i += 1 - return -2 - - def evalCondSubPrefs(self, condsub_prefs): - log(LOG_DEBUG, 'Evaluating conditional subtitle preferences' ) - log(LOG_DEBUG, 'Subtitle names containing the following keywords are blacklisted: {0}'.format(','.join(settings.keyword_blacklist))) - # if the audio track has been changed wait some time - if (self.audio_changed and settings.delay > 0): - log(LOG_DEBUG, "Delaying preferences evaluation by {0} ms".format(4*settings.delay)) - xbmc.sleep(4*settings.delay) - log(LOG_DEBUG, 'Getting video properties') - self.getDetails() - i = 0 - for pref in condsub_prefs: - i += 1 - g_t, preferences = pref - # genre or tags are given (g_t not empty) but none of them matches the video's tags/genres - if g_t and (not (self.genres_and_tags & g_t)): - continue - - log(LOG_INFO,'Cond Sub: genre/tag preference {0} met with intersection {1}'.format(g_t, (self.genres_and_tags & g_t))) - for pref in preferences: - audio_name, audio_code, sub_name, sub_code, forced = pref - if (audio_code is None): - log(LOG_DEBUG,'continue') - continue - - if (self.selected_audio_stream and - 'language' in self.selected_audio_stream and - (audio_code == self.selected_audio_stream['language'] or audio_name == self.selected_audio_stream['language'] or audio_code == "any")): - log(LOG_INFO, 'Selected audio language matches conditional preference {0} ({1}:{2}), force tag is {3}'.format(i, audio_name, sub_name, forced) ) - if (sub_code == "non"): - if (forced == 'true'): - log(LOG_INFO, 'Subtitle condition is None but forced is true, searching a forced subtitle matching selected audio...') - for sub in self.subtitles: - log(LOG_DEBUG, 'Looping subtitles...') - if ((audio_code == sub['language']) or (audio_name == sub['language'])): - log(LOG_DEBUG, 'One potential match found...') - if (self.testForcedFlag(forced, sub['name'], sub['isforced'])): - log(LOG_DEBUG, 'One forced match found...') - log(LOG_INFO, 'Language of subtitle {0} matches audio preference {1} ({2}:{3}) with forced overriding rule {4}'.format((sub['index']+1), i, audio_name, sub_name, forced) ) - return sub['index'] - log(LOG_INFO, 'Conditional subtitle: no match found for preference {0} ({1}:{2}) with forced overriding rule {3}'.format(i, audio_name, sub_name, forced) ) - return -1 - else: - for sub in self.subtitles: - if (settings.keyword_blacklist_enabled and any(keyword in sub['name'].lower() for keyword in settings.keyword_blacklist)): - log(LOG_INFO,'CondSubs : one subtitle track is found matching Keyword Blacklist : {0}. Skipping it.'.format(','.join(settings.keyword_blacklist))) - continue - if (settings.ignore_signs_on and self.isSignsSub(sub['name'])): - log(LOG_INFO,'CondSubs : ignore_signs toggle is on and one such subtitle track is found. Skipping it.') - continue - if ((sub_code == sub['language']) or (sub_name == sub['language'])): - if (self.testForcedFlag(forced, sub['name'], sub['isforced'])): - log(LOG_INFO, 'Language of subtitle {0} matches conditional preference {1} ({2}:{3}) forced {4}'.format((sub['index']+1), i, audio_name, sub_name, forced) ) - return sub['index'] - log(LOG_INFO, 'Conditional subtitle: no match found for preference {0} ({1}:{2})'.format(i, audio_name, sub_name) ) - i += 1 - return -2 - - def isSignsSub(self, subName): - test = subName.lower() - matches = ['signs'] - return any(x in test for x in matches) - - def testForcedFlag(self, forced, subName, subForcedTag): - test = subName.lower() - matches = ['forced', 'forcés'] - found = any(x in test for x in matches) - # Only when looking for forced subs : - # in case the sub name is plain empty or not well documented, - # check also the sub isforced tag and consider it a match if set - if (forced and not found and subForcedTag): - found = True - return ((forced == 'false') and not found) or ((forced == 'true') and found) - - def isExternalSub(self, subName): - test = subName.lower() - matches = ['ext'] - return any(x in test for x in matches) - - def getDetails(self): - activePlayers ='{"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1}' - json_query = xbmc.executeJSONRPC(activePlayers) - #json_query = unicode(json_query, 'utf-8', errors='ignore') - json_response = simplejson.loads(json_query) - activePlayerID = json_response['result'][0]['playerid'] - details_query_dict = { "jsonrpc": "2.0", - "method": "Player.GetProperties", - "params": { "properties": - ["currentaudiostream", "audiostreams", "subtitleenabled", - "currentsubtitle", "subtitles" ], - "playerid": activePlayerID }, - "id": 1} - details_query_string = simplejson.dumps(details_query_dict) - json_query = xbmc.executeJSONRPC(details_query_string) - #json_query = unicode(json_query, 'utf-8', errors='ignore') - json_response = simplejson.loads(json_query) - - if 'result' in json_response and json_response['result'] != None: - self.selected_audio_stream = json_response['result']['currentaudiostream'] - self.selected_sub = json_response['result']['currentsubtitle'] - self.selected_sub_enabled = json_response['result']['subtitleenabled'] - self.audiostreams = json_response['result']['audiostreams'] - self.subtitles = json_response['result']['subtitles'] - log(LOG_DEBUG, json_response ) - - if (not settings.custom_condsub_prefs_on and not settings.custom_audio_prefs_on and not settings.custom_sub_prefs_on): - log(LOG_DEBUG, 'No custom prefs used at all, skipping extra Video tags/genres JSON query.') - self.genres_and_tags = set() - return - - genre_tags_query_dict = {"jsonrpc": "2.0", - "method": "Player.GetItem", - "params": { "properties": - ["genre", "tag"], - "playerid": activePlayerID }, - "id": 1} - genre_tags_query_string = simplejson.dumps(genre_tags_query_dict) - json_query = xbmc.executeJSONRPC(genre_tags_query_string) - #json_query = unicode(json_query, 'utf-8', errors='ignore') - json_response = simplejson.loads(json_query) - if 'result' in json_response and json_response['result'] != None: - gt = [] - if 'genre' in json_response['result']['item']: - gt = json_response['result']['item']['genre'] - if 'tag' in json_response['result']['item']: - gt.extend(json_response['result']['item']['tag']) - self.genres_and_tags = set(map(lambda x:x.lower(), gt)) - log(LOG_DEBUG, 'Video tags/genres: {0}'.format(self.genres_and_tags)) - log(LOG_DEBUG, json_response ) - diff --git a/service.languagepreferencemanager/resources/settings.xml b/service.languagepreferencemanager/resources/settings.xml deleted file mode 100644 index 0b92ffb63..000000000 --- a/service.languagepreferencemanager/resources/settings.xml +++ /dev/null @@ -1,842 +0,0 @@ - -
- - - - false - - - - 1 - - - - - - - - - - - - - - false - - - - false - - - - false - - - - audiostream[_|.|-]*\d+|subtitle[_|.|-]*\d+ - - false - - - 30117 - - - - 300 - - 30112 - - - - false - - - - false - - - - - - false - - - - - - true - - - 30126 - - false - - true - - - - - - true - - - 30127 - - - true - - - - - - - - false - - - - - - 15 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 11 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 45 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - - - 30118 - - - - - - - - false - - - - - - 15 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - - - - - - 11 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - - - - - - 45 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - - - - - - - - true - - - 30119 - - - - - - - - false - - - - - - 15 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 45 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - - - - - - 11 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 15 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - - - - - - 11 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 11 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - - - - - - - - true - - - 30121 - - - - -
-
diff --git a/service.watchedlist/LICENSE.txt b/service.watchedlist/LICENSE.txt deleted file mode 100644 index 94a9ed024..000000000 --- a/service.watchedlist/LICENSE.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -them 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 prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey 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; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - 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. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -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. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/service.watchedlist/addon.xml b/service.watchedlist/addon.xml deleted file mode 100644 index 1c65ac43d..000000000 --- a/service.watchedlist/addon.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - executable - - - - - Save watched state of movies and TV-series episodes independent from filenames - Gesehen-Status von Filmen und Serien-Episoden unabhängig von Dateinamen speichern - - Export: Searches the Kodi-Database for watched files. Determine imdb-id and thetvdb-id to identify movies and TV-episodes. Then save the list to a new independent table. - Import: Set the watched state for each video file in Kodi. - Automatic background process without user interaction. - - Export: Durchsucht die Kodi-Datenbank nach gesehenen Filmen und ordnet den Dateien die eindeutigen Kennnummern von imdb und TheTVDB zu. Gesehene Filme und Episoden werden jeweils in einer eigenen Datenbank gespeichert. - Import: Der Gesehen-Status von Medien in Kodi wird aus der Gesehenen-Tabelle geholt. - Die Synchronisierung geschieht automatisch im Hintergrund ohne Benutzereingriff. - This script does not regard the file format. A Movie watched in DVD quality will also set the 1080p version as watched. - Dieses Script ignoriert das Dateiformat. Ein gesehener Film in DVD-Qualität wird auch in der BlueRay-Version als gesehen gekennzeichnet. - all - GPL-2.0-or-later - https://forum.kodi.tv/showthread.php?tid=164652 - https://kodi.wiki/view/Add-on:WatchedList - https://github.com/SchapplM/xbmc-addon-service-watchedlist - 1.3.6 - Dependency update for Kodi Nexus - - icon.png - fanart.jpg - - - diff --git a/service.watchedlist/changelog.txt b/service.watchedlist/changelog.txt deleted file mode 100644 index f97056bf0..000000000 --- a/service.watchedlist/changelog.txt +++ /dev/null @@ -1,109 +0,0 @@ -version 1.3.5 (28.11.2021) -* Fix for inconsistent imdb/tvdb numbers and new database format - -version 1.3.4 (23.10.2021) -* Fix for The Movie Database scraper - -version 1.3.3 (01.08.2021) -* Fix for new python interface in Kodi v20 - -version 1.3 (24.04.2020) -* Updated to work with Python 3 - -version 1.2.7 (03.05.2019) -* Minor fixes (mysql access protection, remove credentials from log file) - -version 1.2.6 (16.02.2019) -* Fixed waiting time for periodic update during playback - -version 1.2.5 (04.01.2018) -* Fix SMB access in Windows, minor code improvements - -version 1.2.4 (01.01.2018) -* Fix in background user watching - -version 1.2.3 (28.12.2017) -* Minor fixes (unicode support, less blocking on shutdown, backup file rotation on network shares, error handling and logging) - -version 1.2.2 (23.12.2017) -* Minor fixes, fixed buggalo emails - -version 1.2.1 (26.11.2017) -* Fixed Dropbox API: Change from v1 to v2 - -version 1.2 (21.05.2017) -* Added API and python module to use this addon in other addons -* minor fixes - -version 1.1.4 (28.04.2017) - * Fixed error in English language file - -version 1.1.3 (16.04.2017) - * Added verbosity option for graphical user notifications - * Added limitation of number of backup files - -version 1.1.2 (12.06.2016) - * Added fix to avoid infinite waiting if startup delay time is 0 - -version 1.1.1 (29.05.2016) - * Added fix for non-working python dropbox import - -version 1.1.0 (02.04.2016) - * Added dropbox synchronization for the database - -version 1.0.5 (24.12.2015) - * Enabled IPv6 adresses for hostnames - -version 1.0.4 (16.05.2015) - * Fixed mysql timestamp bug - -version 1.0.3 (11.01.2015) - * Fixed wrong implementation of startup delay - * Fixed missing startup execution if autostart mode is set to periodic - -version 1.0.2 (10.01.2015) - * Fixed support for Database file got lost in version 1.0.0 - -version 1.0.1 (13.12.2014) - * Fixed unwanted waiting time in autostart - -version 1.0.0 (01.09.2014) - * Added mySQL Database support for the WatchedList Database - -version 0.9.8 (31.08.2014) - * Fix for Unicode Path problems - * Fix for SQLite Error handling - -version 0.9.7 (14.06.2014) - * Added custom sleep function to handle requested shutdown in Gotham - -version 0.9.6 (23.09.2013) - * Fixed tvshow id with imdb-scraper - -version 0.9.5 (18.09.2013) - * Fixed bug in watching user actions - -version 0.9.4 (09.09.2013) - * Fixed bug in watching user actions - -version 0.9.3 (04.09.2013) - * Fixed minor bugs - -version 0.9.2 (01.08.2013) - * Fixed minor bugs - -version 0.9.1 (24.06.2013) - * Fixed: Network share problems in Linux - -version 0.9.0 (09.06.2013) - * Added: Check watched-states permanently and enable to set media as unwatched - -version 0.8.1 (22.05.2013) - * Fixed: Duplicate movies and tv episodes get marked as watched - -version 0.8.0 (16.05.2013) - * Fixed: lastplayed-information is now also stored in the database (although it is sometimes missing in XBMC) - * Added: Exceptions are catched with buggalo and bug reports are sent - -version 0.7.0 (11.05.2013) - * Initial release diff --git a/service.watchedlist/default.py b/service.watchedlist/default.py deleted file mode 100644 index b7c768b54..000000000 --- a/service.watchedlist/default.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -This file is entry point for automatic start via Kodi -""" - -import sys - -import xbmc - -import lib.watchedlist.utils as utils -from lib.watchedlist.watchedlist import WatchedList - -__remotedebug__ = False -# Append pydev remote debugger -if __remotedebug__: - utils.log("Initialize remote debugging.") - # Make pydev debugger works for auto reload. - try: - import pydevd - pydevd.settrace('localhost', port=60678, stdoutToServer=True, stderrToServer=True) - except ImportError: - sys.stderr.write("Error: " + - "You must add org.python.pydev.debug.pysrc to your PYTHONPATH.") - utils.showNotification('WatchedList Error', 'remote debug could not be imported.', xbmc.LOGERROR) - sys.exit(1) - except BaseException: - utils.showNotification('WatchedList Error', 'remote debug in pydev is activated, but remote server not responding.', xbmc.LOGERROR) - sys.exit(1) - -# Run the program -utils.log("WatchedList Database Service starting...") -WatchedList().runProgram() diff --git a/service.watchedlist/fanart.jpg b/service.watchedlist/fanart.jpg deleted file mode 100644 index 55fbe5380..000000000 Binary files a/service.watchedlist/fanart.jpg and /dev/null differ diff --git a/service.watchedlist/icon.png b/service.watchedlist/icon.png deleted file mode 100644 index 56846ad34..000000000 Binary files a/service.watchedlist/icon.png and /dev/null differ diff --git a/service.watchedlist/lib/watchedlist/api.py b/service.watchedlist/lib/watchedlist/api.py deleted file mode 100644 index 4685edef6..000000000 --- a/service.watchedlist/lib/watchedlist/api.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -This file contains API functions for the usage of WatchedList with other Kodi addons -""" - -import time - -import xbmc - -from lib.watchedlist.watchedlist import WatchedList -import lib.watchedlist.utils as utils - - -def change_watched_movie(imdb_id, playCount=1, name=''): - """ - Change the watched state of one movie in the WL database - - Args: - imdb_id: ID of the movie in the imdb. Format: Integer, numbers after 'tt0012345' - playCount: Unsigned Integer - name: Title of the movie (optional) - - Returns: - Error Code (0=No error) - """ - - if not name: - name = 'tt%d' % imdb_id - if playCount == 0: - lastPlayed = 0 - else: - lastPlayed = int(time.time()) - row_xbmc = [imdb_id, 0, 0, lastPlayed, playCount, name, 0] # 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid - saveanyway = True - commit = True - lastChange = lastPlayed - with WatchedList(True) as WL: - if WL.get_watched_wl(1): # Read the WL database - utils.showNotification(utils.getString(32102), utils.getString(32602), xbmc.LOGERROR) - return 1 - WL.wl_update_media('movie', row_xbmc, saveanyway, commit, lastChange) - return 0 - - -def change_watched_episode(tvdb_id, season, episode, playCount=1, name=''): - """ - Change the watched state of one episode in the WL database - - Args: - tvdb_id: ID of the tv show in tvdb. Format: Integer - season: Number of the season (Integer) within the show - episode: Number of the episode within the season (Integer) - playCount: Unsigned Integer - name: Name of the Episode (optional) - - Returns: - Error Code (0=No error) - """ - - if not name: - name = 'tvdb%d S%02dE%02d' % (tvdb_id, season, episode) - if playCount == 0: - lastPlayed = 0 - else: - lastPlayed = int(time.time()) - row_xbmc = [tvdb_id, season, episode, lastPlayed, playCount, name, 0] # 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid - saveanyway = True - commit = True - lastChange = lastPlayed - with WatchedList(True) as WL: - if WL.get_watched_wl(1): # Read the WL database - utils.showNotification(utils.getString(32102), utils.getString(32602), xbmc.LOGERROR) - return 1 - WL.wl_update_media('episode', row_xbmc, saveanyway, commit, lastChange) - return 0 - - -def change_watched_episodes(tvdb_id, seasons, episodes, playCount, names): - """ - Change the watched state of multiple episodes of one tv show in the WL database - - Args: - tvdb_id: ID of the tv show in tvdb. Format: Integer - seasons: Number of the seasons for the affected episodes (list of Integers) - episodes: Number of the episodes within the seasons (list of Integers, same length as seasons list). - playCount: Unsigned Integer - names: Names of the Episodes (list of strings, same length as lists above) - - Returns: - Error Code (0=No error) - """ - - if playCount == 0: - lastPlayed = 0 - else: - lastPlayed = int(time.time()) - with WatchedList(True) as WL: - if WL.get_watched_wl(1): # Read the WL database - utils.showNotification(utils.getString(32102), utils.getString(32602), xbmc.LOGERROR) - return 1 - for i in range(len(seasons)): - row_xbmc_i = [tvdb_id, seasons[i], episodes[i], lastPlayed, playCount, names[i], 0] # 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid - saveanyway = True - if i == len(seasons) - 1: - commit = True # Only commit the last row to reduce file access to the database - else: - commit = False - lastChange = lastPlayed - - WL.wl_update_media('episode', row_xbmc_i, saveanyway, commit, lastChange) - return 0 diff --git a/service.watchedlist/lib/watchedlist/utils.py b/service.watchedlist/lib/watchedlist/utils.py deleted file mode 100644 index c49f88926..000000000 --- a/service.watchedlist/lib/watchedlist/utils.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -This file contains additional utility functions -""" -import os -import time -import re -import random -import json - -import xbmc -import xbmcgui -import xbmcvfs -import xbmcaddon -import buggalo - -_addon_id = u'service.watchedlist' -_Addon = xbmcaddon.Addon(_addon_id) - - -def data_dir(): - """"get user data directory of this addon. - according to http://wiki.xbmc.org/index.php?title=Add-on_Rules#Requirements_for_scripts_and_plugins - """ - _datapath = xbmcvfs.translatePath(_Addon.getAddonInfo('profile')) - if not xbmcvfs.exists(_datapath): - xbmcvfs.mkdir(_datapath) - return _datapath - - -def addon_dir(): - """"get source directory of this addon. - according to http://wiki.xbmc.org/index.php?title=Add-on_Rules#Requirements_for_scripts_and_plugins - """ - return _Addon.getAddonInfo('path') - - -def log(message, loglevel=xbmc.LOGINFO): - """"save message to xbmc.log. - - Args: - message: has to be unicode, http://wiki.xbmc.org/index.php?title=Add-on_unicode_paths#Logging - loglevel: xbmc.LOGDEBUG, xbmc.LOGINFO, xbmc.LOGWARNING, xbmc.LOGERROR, xbmc.LOGFATAL - """ - xbmc.log(_addon_id + u": " + message, level=loglevel) - - -def showNotification(title, message, loglevel, showtime=4000): - """Show Notification - - Args: - title: has to be unicode - message: has to be unicode - loglevel: Log-level of the message (equivalent to xbmc.LOGDEBUG ... xbmc.LOGFATAL) - showtime: Time that the message is beeing displayed - """ - # Check log level - if getSetting('verbosity') == '1' and loglevel < xbmc.LOGINFO: - return # setting "only infos" - elif getSetting('verbosity') == '2' and loglevel < xbmc.LOGWARNING: - return # setting "only warnings" - elif getSetting('verbosity') == '3' and loglevel < xbmc.LOGERROR: - return # setting "only errors" - elif getSetting('verbosity') == '4': - return # setting "None" - _addoniconpath = _Addon.getAddonInfo('icon') - log(u'Notification. %s: %s' % (title, message)) - if not xbmc.Player().isPlaying(): # do not show the notification, if a video is being played. - xbmcgui.Dialog().notification(title, message, _addoniconpath, showtime) - - -def setSetting(name, value): - _Addon.setSetting(name, value) - - -def getSetting(name): - # The returned value from getSetting is not unicode, only string - # This function returns unicode - return _Addon.getSetting(name) - - -def getString(string_id): - # return a localized string from resources/language/*.po - # The returned string is unicode - return _Addon.getLocalizedString(string_id) - -def footprint(): - """Print settings to log file""" - log(u'data_dir() = %s' % data_dir(), xbmc.LOGDEBUG) - log(u'addon_dir() = %s' % addon_dir(), xbmc.LOGDEBUG) - log(u'verbosity = %s' % getSetting('verbosity'), xbmc.LOGDEBUG) - log(u'w_movies = %s' % getSetting('w_movies'), xbmc.LOGDEBUG) - log(u'w_episodes = %s' % getSetting('w_episodes'), xbmc.LOGDEBUG) - log(u'autostart = %s' % getSetting('autostart'), xbmc.LOGDEBUG) - log(u'delay = %s' % getSetting('delay'), xbmc.LOGDEBUG) - log(u'starttype = %s' % getSetting('starttype'), xbmc.LOGDEBUG) - log(u'interval = %s' % getSetting('interval'), xbmc.LOGDEBUG) - log(u'watch_user = %s' % getSetting('watch_user'), xbmc.LOGDEBUG) - log(u'progressdialog = %s' % getSetting('progressdialog'), xbmc.LOGDEBUG) - log(u'db_format = %s' % getSetting('db_format'), xbmc.LOGDEBUG) - log(u'extdb = %s' % getSetting('extdb'), xbmc.LOGDEBUG) - log(u'dbpath = %s' % getSetting('dbpath'), xbmc.LOGDEBUG) - log(u'dbfilename = %s' % getSetting('dbfilename'), xbmc.LOGDEBUG) - log(u'dbbackup = %s' % getSetting('dbbackup'), xbmc.LOGDEBUG) - log(u'dropbox_enabled = %s' % getSetting('dropbox_enabled'), xbmc.LOGDEBUG) - log(u'dropbox_apikey = %s' % getSetting('dropbox_apikey'), xbmc.LOGDEBUG) - log(u'mysql_server = %s' % getSetting('mysql_server'), xbmc.LOGDEBUG) - log(u'mysql_port = %s' % getSetting('mysql_port'), xbmc.LOGDEBUG) - log(u'mysql_user = *******', xbmc.LOGDEBUG) - log(u'mysql_pass = *******', xbmc.LOGDEBUG) - - -def sqlDateTimeToTimeStamp(sqlDateTime): - """Convert SQLite DateTime to Unix Timestamp - - Args: - sqlDateTime: E.g. "2013-05-10 21:23:24" - Returns: - timestamp: E.g. 1368213804 - """ - # sqlDateTime is a string (only from SQLite db. Mysql returns object) - if sqlDateTime == '': - return 0 # NULL timestamp - else: - # the usage of strptime produces the error "Failed to import _strptime because the import lock is held by another thread." - # to solve this, in case of error try again after random time - try: - for i in range(5): - try: - return int(time.mktime(time.strptime(sqlDateTime, "%Y-%m-%d %H:%M:%S"))) - except BaseException: - xbmc.wait(random.randint(200, 500)) - except BaseException: - return 0 # error, but timestamp=0 works in the addon - - -def TimeStamptosqlDateTime(TimeStamp): - """Convert Unix Timestamp to SQLite DateTime - - Args: - timestamp: E.g. 1368213804 - - Returns: - sqlDateTime: E.g. "2013-05-10 21:23:24" - """ - if TimeStamp == 0: - return "" - return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(TimeStamp)) - - -def executeJSON(request): - """Execute JSON-RPC Command - - Args: - request: Dictionary with JSON-RPC Commands - """ - rpccmd = json.dumps(request) # create string from dict - json_query = xbmc.executeJSONRPC(rpccmd) - json_response = json.loads(json_query) - # in case of exception this will be sent - buggalo.addExtraData('len(json_query)', len(json_query)) - buggalo.addExtraData('len(json_response)', len(json_response)) - return json_response - - -def buggalo_extradata_settings(): - """"add extradata to buggalo""" - - buggalo.addExtraData('data_dir', data_dir()) - buggalo.addExtraData('addon_dir', addon_dir()) - buggalo.addExtraData('setting_verbosity', getSetting("verbosity")) - buggalo.addExtraData('setting_w_movies', getSetting("w_movies")) - buggalo.addExtraData('setting_w_episodes', getSetting("w_episodes")) - buggalo.addExtraData('setting_autostart', getSetting("autostart")) - buggalo.addExtraData('setting_delay', getSetting("delay")) - buggalo.addExtraData('setting_starttype', getSetting("starttype")) - buggalo.addExtraData('setting_interval', getSetting("interval")) - buggalo.addExtraData('setting_progressdialog', getSetting("progressdialog")) - buggalo.addExtraData('setting_watch_user', getSetting("watch_user")) - buggalo.addExtraData('setting_extdb', getSetting("extdb")) - buggalo.addExtraData('setting_dbpath', getSetting("dbpath")) - buggalo.addExtraData('setting_dbfilename', getSetting("dbfilename")) - buggalo.addExtraData('setting_dbbackup', getSetting("dbbackup")) - buggalo.addExtraData('setting_db_format', getSetting("db_format")) - buggalo.addExtraData('setting_mysql_server', getSetting("mysql_server")) - buggalo.addExtraData('setting_mysql_port', getSetting("mysql_port")) - buggalo.addExtraData('setting_mysql_db', getSetting("mysql_db")) - - -def fileaccessmode(path): - """"determine file access mode for the given path - in case of network shares no direct access is possible - on windows, smb paths can be accessed directly in certain conditions, - which are ignored for the sake of simplicity - - Args: - path: Path to File - - Returns: - copy_mode: Mode of file access: 'copy' or 'normal' - """ - - res_nw = re.compile(r'(.*?)://(.*?)').findall(path) - if res_nw: - # Path with smb://, nfs:// or ftp:// is correct, but can not be accessed with normal python file access. - # Copy the file with the virtual file system - return 'copy' - else: - # "normal" path on local filesystem - return 'normal' diff --git a/service.watchedlist/lib/watchedlist/watchedlist.py b/service.watchedlist/lib/watchedlist/watchedlist.py deleted file mode 100644 index a165ba97a..000000000 --- a/service.watchedlist/lib/watchedlist/watchedlist.py +++ /dev/null @@ -1,1846 +0,0 @@ -""" -This file contains the class of the addon - -Settings for this addon: -w_movies - 'true', 'false': save watched state of movies -w_episodes - 'true', 'false': save watched state of movies -autostart -delay - delay after startup in minutes: '0', '5', '10', ... -starttype - '0' = No autostart - '1' = One Execution after Kodi start - '2' = Periodic start of the addon -interval -watch_user -progressdialog -verbosity - '0' = All messages (debug) - '1' = Only info / notification - '2' = Only warnings - '3' = Only errors - '4' = No Messages -db_format - '0' = SQLite File - '1' = MYSQL Server -drobox_enabled -extdb - 'true', 'false': Use external database file -dbpath - String: Specify path to external database file -dbfilename -dbbackupcount -mysql_server -mysql_port -mysql_db -mysql_user -mysql_pass -""" - -import sys -import os -import re -import time -import sqlite3 -import base64 - -import xbmc -import xbmcgui -import xbmcvfs -import mysql.connector -import buggalo - -import lib.watchedlist.utils as utils - -try: - import dropbox - from dropbox.exceptions import ApiError as DropboxApiError - DROPBOX_APP_KEY = base64.b64decode("YmhkMnY4aGdzbXF3Y2d0").decode('ascii') - DROPBOX_APP_SECRET = base64.b64decode("dDJjZXBvZXZqcXl1Ym5k").decode('ascii') - DROPBOX_ENABLED = True -except BaseException: - DROPBOX_ENABLED = False - if utils.getSetting("dropbox_enabled") == 'true': - utils.showNotification(utils.getString(32708), utils.getString(32720), xbmc.LOGWARNING) - -if utils.getSetting('dbbackupcount') != '0': - import zipfile - import datetime - -buggalo.EMAIL_CONFIG = {"recipient": base64.b64decode("bXNhaGFkbDYwQGdtYWlsLmNvbQ==").decode('ascii'), - "sender": base64.b64decode("QnVnZ2FsbyA8a29kaXdhdGNoZWRsaXN0QGdtYWlsLmNvbT4=").decode('ascii'), - "server": base64.b64decode("c210cC5nb29nbGVtYWlsLmNvbQ==").decode('ascii'), - "method": "ssl", - "user": base64.b64decode("a29kaXdhdGNoZWRsaXN0QGdtYWlsLmNvbQ==").decode('ascii'), - "pass": base64.b64decode("bWNneHd1and6c3ducW1iaA==").decode('ascii')} - -QUERY_MV_INSERT_SQLITE = 'INSERT OR IGNORE INTO movie_watched (idMovieImdb,playCount,lastChange,lastPlayed,title) VALUES (?, ?, ?, ?, ?)' -QUERY_MV_INSERT_MYSQL = 'INSERT IGNORE INTO movie_watched (idMovieImdb,playCount,lastChange,lastPlayed,title) VALUES (%s, %s, FROM_UNIXTIME(%s), FROM_UNIXTIME(%s), %s)' -QUERY_EP_INSERT_SQLITE = 'INSERT OR IGNORE INTO episode_watched (idShow,season,episode,playCount,lastChange,lastPlayed) VALUES (?, ?, ?, ?, ?, ?)' -QUERY_EP_INSERT_MYSQL = 'INSERT IGNORE INTO episode_watched (idShow,season,episode,playCount,lastChange,lastPlayed) VALUES (%s, %s, %s, %s, FROM_UNIXTIME(%s), FROM_UNIXTIME(%s))' - -QUERY_MV_UPDATE_SQLITE = 'UPDATE movie_watched SET playCount = ?, lastplayed = ?, lastChange = ? WHERE idMovieImdb LIKE ?' -QUERY_MV_UPDATE_MYSQL = 'UPDATE movie_watched SET playCount = %s, lastplayed = FROM_UNIXTIME(%s), lastChange = FROM_UNIXTIME(%s) WHERE idMovieImdb LIKE %s' -QUERY_EP_UPDATE_SQLITE = 'UPDATE episode_watched SET playCount = ?, lastPlayed = ?, lastChange = ? WHERE idShow LIKE ? AND season LIKE ? AND episode LIKE ?' -QUERY_EP_UPDATE_MYSQL = 'UPDATE episode_watched SET playCount = %s, lastPlayed = FROM_UNIXTIME(%s), lastChange = FROM_UNIXTIME(%s) WHERE idShow LIKE %s AND season LIKE %s AND episode LIKE %s' - -# Queries to create tables for movies ("mv"), episodes ("ep") and series ("ss") for sqlite and mysql -QUERY_CREATE_MV_SQLITE = "CREATE TABLE IF NOT EXISTS movie_watched (idMovieImdb INTEGER PRIMARY KEY,playCount INTEGER,lastChange INTEGER,lastPlayed INTEGER,title TEXT)" -QUERY_CREATE_EP_SQLITE = "CREATE TABLE IF NOT EXISTS episode_watched (idShow INTEGER, season INTEGER, episode INTEGER, playCount INTEGER,lastChange INTEGER,lastPlayed INTEGER, PRIMARY KEY (idShow, season, episode))" -QUERY_CREATE_SS_SQLITE = "CREATE TABLE IF NOT EXISTS tvshows (idShow INTEGER, title TEXT, PRIMARY KEY (idShow))" - -QUERY_CREATE_MV_MYSQL = ("CREATE TABLE IF NOT EXISTS `movie_watched` (" - "`idMovieImdb` int unsigned NOT NULL," - "`playCount` tinyint unsigned DEFAULT NULL," - "`lastChange` timestamp NULL DEFAULT NULL," - "`lastPlayed` timestamp NULL DEFAULT NULL," - "`title` text," - "PRIMARY KEY (`idMovieImdb`)" - ") ENGINE=InnoDB DEFAULT CHARSET=utf8;") -QUERY_CREATE_EP_MYSQL = ("CREATE TABLE IF NOT EXISTS `episode_watched` (" - "`idShow` int unsigned NOT NULL DEFAULT '0'," - "`season` smallint unsigned NOT NULL DEFAULT '0'," - "`episode` smallint unsigned NOT NULL DEFAULT '0'," - "`playCount` tinyint unsigned DEFAULT NULL," - "`lastChange` timestamp NULL DEFAULT NULL," - "`lastPlayed` timestamp NULL DEFAULT NULL," - "PRIMARY KEY (`idShow`,`season`,`episode`)" - ") ENGINE=InnoDB DEFAULT CHARSET=utf8;") -QUERY_CREATE_SS_MYSQL = ("CREATE TABLE IF NOT EXISTS `tvshows` (" - "`idShow` int unsigned NOT NULL," - "`title` text," - "PRIMARY KEY (`idShow`)" - ") ENGINE=InnoDB DEFAULT CHARSET=utf8;") - -# Queries for clearing data from the tables -QUERY_CLEAR_MV_SQLITE = "DELETE FROM movie_watched;" -QUERY_CLEAR_EP_SQLITE = "DELETE FROM episode_watched;" - -# Queries for selecting all table entries -QUERY_SELECT_MV_SQLITE = "SELECT idMovieImdb, lastPlayed, playCount, title, lastChange FROM movie_watched ORDER BY title" -QUERY_SELECT_MV_MYSQL = "SELECT `idMovieImdb`, UNIX_TIMESTAMP(`lastPlayed`), `playCount`, `title`, UNIX_TIMESTAMP(`lastChange`) FROM `movie_watched` ORDER BY `title`" -QUERY_SELECT_EP_SQLITE = "SELECT idShow, season, episode, lastPlayed, playCount, lastChange FROM episode_watched ORDER BY idShow, season, episode" -QUERY_SELECT_EP_MYSQL = "SELECT `idShow`, `season`, `episode`, UNIX_TIMESTAMP(`lastPlayed`), `playCount`, UNIX_TIMESTAMP(`lastChange`) FROM `episode_watched` ORDER BY `idShow`, `season`, `episode`" - -# Queries for inserting tv series -QUERY_INSERT_SS_SQLITE = "INSERT OR IGNORE INTO tvshows (idShow,title) VALUES (?, ?)" -QUERY_INSERT_SS_MYSQL = "INSERT IGNORE INTO tvshows (idShow,title) VALUES (%s, %s)" - - -class WatchedList: - """ - Main class of the add-on - """ - - def __init__(self, externalcall=False): - """ - Initialize Class, default values for all class variables - - Args: - externalcall: Flag if the class was created for use with another Addon (via the API). - In this case some features will be deactivated for speed purpose - """ - self.watchedmovielist_wl = list([]) # 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6lastChange - self.watchedepisodelist_wl = list([]) # 0tvdbnumber, 1season, 2episode, 3lastplayed, 4playcount, 5empty, 6lastChange - - self.watchedmovielist_xbmc = list([]) # 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid - self.watchedepisodelist_xbmc = list([]) # 0tvdbnumber, 1season, 2episode, 3lastplayed, 4playcount, 5name, 6empty, 7episodeid - - self.tvshows = {} # dict: key=xbmcid, value=[tvdbnumber, showname] - self.tvshownames = {} # dict: key=tvdbnumber, value=showname - - self.sqlcon_wl = 0 - self.sqlcursor_wl = 0 - self.sqlcon_db = 0 - self.sqlcursor_db = 0 - - self.db_method = 'file' # either 'file' or 'mysql' - - # flag to remember copying the databasefile if requested - self.dbbackupdone = False - - self.watch_user_changes_count = 0 - - # normal access of files or access over the Kodi virtual file system (on unix) - self.dbfileaccess = 'normal' - - self.dbpath = '' - self.dbdirectory = '' - self.dropbox_path = None - self.downloaded_dropbox_timestamp = 0 - self.dbdirectory_copy = '' - self.dbpath_copy = '' - - # monitor for shutdown detection - self.monitor = xbmc.Monitor() - - if externalcall: - # No Dropbox for external API calls - self.downloaded_dropbox_timestamp = float('inf') - # No database backup - self.dbbackupdone = True - - def __del__(self): - ''' Cleanup db when object destroyed ''' - self.close_db(3) # changes have to be committed before - - def __enter__(self): # for using the class in a with-statement - return self - - def __exit__(self, exc_type, exc_value, traceback): # for using the class in a with-statement - self.close_db(3) # changes have to be committed before - - def runProgram(self): - """ - entry point for automatic start. - Main function to call other functions - infinite loop for periodic database update - - Returns: - return codes: - 0 success - 2 undefined - 3 error/exit - 4 planned exit (no error) - """ - try: - # workaround to disable autostart, if requested - if utils.getSetting("autostart") == 'false': - return 0 - - utils.buggalo_extradata_settings() - utils.footprint() - - # wait the delay time after startup - delaytime = int(utils.getSetting("delay")) * 60 # in seconds - utils.log(u'Delay time before execution: %d seconds' % delaytime, xbmc.LOGDEBUG) - utils.showNotification(utils.getString(32101), utils.getString(32004) % int(utils.getSetting("delay")), xbmc.LOGINFO) - if self.monitor.waitForAbort(1 + delaytime): # wait at least one second (zero waiting time waits infinitely) - return 0 - - # load all databases - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - utils.showNotification(utils.getString(32102), utils.getString(32601), xbmc.LOGERROR) - return 3 - if not self.tvshownames: - if self.sync_tvshows(): - return 2 - if not self.watchedmovielist_wl: - if self.get_watched_wl(1): - return 2 - if not self.watchedmovielist_xbmc: - if self.get_watched_xbmc(1): - return 2 - executioncount = 0 - idletime = 0 - - if utils.getSetting("watch_user") == 'true': - utils.showNotification(utils.getString(32101), utils.getString(32005), xbmc.LOGINFO) - - # handle the periodic execution - while int(utils.getSetting("starttype")) > 0 or utils.getSetting("watch_user") == 'true': - starttime = time.time() - # determine sleeptime before next full watched-database update - if utils.getSetting("starttype") == '1' and executioncount == 0: # one execution after startup - sleeptime = 0 - elif utils.getSetting("starttype") == '2': # periodic execution - if executioncount == 0: # with periodic execution, one update after startup and then periodic - sleeptime = 0 - else: - sleeptime = int(utils.getSetting("interval")) * 3600 # wait interval until next startup in [seconds] - # wait and then update again - utils.log(u'wait %d seconds until next update' % sleeptime) - utils.showNotification(utils.getString(32101), utils.getString(32003) % (sleeptime / 3600), xbmc.LOGINFO) - else: # no autostart, only watch user - sleeptime = 3600 # arbitrary time for infinite loop - - # sleep the requested time and watch user changes - while True: - if self.monitor.abortRequested(): - return 4 - # check if user changes arrived - if utils.getSetting("watch_user") == 'true': - idletime_old = idletime - idletime = xbmc.getGlobalIdleTime() # Kodi idletime in seconds - # check if user could have made changes and process these changes to the wl database - self.watch_user_changes(idletime_old, idletime) - # check if time for update arrived - if time.time() > starttime + sleeptime: - break - if self.monitor.waitForAbort(1): - return 4 # wait 1 second until next check if Kodi terminates and user made changes - # perform full update - if utils.getSetting("starttype") == '1' and executioncount == 0 or utils.getSetting("starttype") == '2': - self.runUpdate(False) - executioncount += 1 - - # check for exiting program - if int(utils.getSetting("starttype")) < 2 and utils.getSetting("watch_user") == 'false': - return 0 # the program may exit. No purpose for background process - - return 0 - except SystemExit: - return 4 - except BaseException: - buggalo.onExceptionRaised() - - def runUpdate(self, manualstart): - """ - entry point for manual start. - this function is called for periodic update for automatic start - perform the update step by step - - Args: - manualstart: True if called manually - - Returns: - return code: - 0 success - 2 SystemExit - 3 Error opening database - 4 Error getting watched state from addon database - 5 Error getting watched state from Kodi database - 6 Error writing WL Database - 7 Error writing Kodi database - 8 Error merging dropbox database into local watched list - 9 Error merging local database into dropbox - 10 Error performing database backup - """ - - try: - utils.buggalo_extradata_settings() - # check if player is running before doing the update. Only do this check for automatic start - while xbmc.Player().isPlaying() and not manualstart: - if self.monitor.waitForAbort(60): - return 1 # wait one minute until next check for active playback - if not xbmc.Player().isPlaying(): - if self.monitor.waitForAbort(180): - return 1 # wait 3 minutes so the dialogue does not pop up directly after the playback ends - - # load the addon-database - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(True): # True: Manual start - utils.showNotification(utils.getString(32102), utils.getString(32601), xbmc.LOGERROR) - return 3 - - if self.sync_tvshows(): - utils.showNotification(utils.getString(32102), utils.getString(32604), xbmc.LOGERROR) - return 5 - - # get the watched state from the addon - if self.get_watched_wl(0): - utils.showNotification(utils.getString(32102), utils.getString(32602), xbmc.LOGERROR) - return 4 - - # get watched state from xbmc - if self.get_watched_xbmc(0): - utils.showNotification(utils.getString(32102), utils.getString(32603), xbmc.LOGERROR) - return 5 - - if self.sync_tvshows(): - utils.showNotification(utils.getString(32102), utils.getString(32604), xbmc.LOGERROR) - return 5 - - # attempt to merge the database from dropbox - if DROPBOX_ENABLED and utils.getSetting("dropbox_enabled") == 'true' and self.merge_dropbox_local(): - utils.showNotification(utils.getString(32102), utils.getString(32607), xbmc.LOGERROR) - # do not abort execution of the whole addon if dropbox fails (e.g. due to network issues) - # return 8 - - # import from Kodi into addon database - res = self.write_wl_wdata() - if res == 2: # user exit - return 0 - elif res == 1: # error - utils.showNotification(utils.getString(32102), utils.getString(32604), xbmc.LOGERROR) - return 6 - - # export from addon database into Kodi database - res = self.write_xbmc_wdata((utils.getSetting("progressdialog") == 'true'), 2) - if res == 2: # user exit - return 0 - elif res == 1: # error - utils.showNotification(utils.getString(32102), utils.getString(32605), xbmc.LOGERROR) - return 7 - - utils.showNotification(utils.getString(32101), utils.getString(32107), xbmc.LOGINFO) - utils.log(u'runUpdate exited with success', xbmc.LOGDEBUG) - - # sync with dropbox - if DROPBOX_ENABLED and utils.getSetting("dropbox_enabled") == 'true': - if self.merge_local_dropbox() == 0: - self.close_db(2) # save the database to file. - self.pushToDropbox() - else: - return 9 - - # close the watchedlist database. Is only closed locally in case of error - self.close_db(1) - - # delete old backup files of the database - # do this at the end for not destroying valuable backup in case something went wrong before - if self.database_backup_delete(): - return 10 - return 0 - except SystemExit: - return 2 - except BaseException: - buggalo.onExceptionRaised() - - def load_db(self, manualstart=False): - """Load WL database - - Args: - manualstart: True if called manually; only retry opening db once - - Returns: - return code: - 0 successfully opened database - 1 error - 2 shutdown (serious error in subfunction) - """ - - try: - if int(utils.getSetting("db_format")) != 1: - # SQlite3 database in a file - # load the db path - if utils.getSetting("extdb") == 'false': - # use the default file - self.dbdirectory = xbmcvfs.translatePath(utils.data_dir()) - buggalo.addExtraData('dbdirectory', self.dbdirectory) - self.dbpath = os.path.join(self.dbdirectory, "watchedlist.db") - else: - wait_minutes = 1 # retry waittime if db path does not exist/ is offline - - while not self.monitor.abortRequested(): - # use a user specified file, for example to synchronize multiple clients - self.dbdirectory = xbmcvfs.translatePath(utils.getSetting("dbpath")) - self.dbfileaccess = utils.fileaccessmode(self.dbdirectory) - self.dbpath = os.path.join(self.dbdirectory, utils.getSetting("dbfilename")) - - if not xbmcvfs.exists(self.dbdirectory): # do not use os.path.exists to access smb:// paths - if manualstart: - utils.log(u'db path does not exist: %s' % self.dbdirectory, xbmc.LOGWARNING) - return 1 # raise error on manual start if directory not accessible (we do not want to wait in that case) - else: - utils.log(u'db path does not exist, wait %d minutes: %s' % (wait_minutes, self.dbdirectory), xbmc.LOGWARNING) - - utils.showNotification(utils.getString(32102), utils.getString(32002) % self.dbdirectory, xbmc.LOGWARNING) - # Wait "wait_minutes" minutes until next check for file path (necessary on network shares, that are offline) - wait_minutes += wait_minutes # increase waittime until next check - if self.monitor.waitForAbort(wait_minutes * 60): - return 2 - else: - break # directory exists, continue below - - # on unix, smb-shares can not be accessed with sqlite3 --> copy the db with Kodi file system operations and work in mirror directory - buggalo.addExtraData('dbfileaccess', self.dbfileaccess) - buggalo.addExtraData('dbdirectory', self.dbdirectory) - buggalo.addExtraData('dbpath', self.dbpath) - if self.dbfileaccess == 'copy': - self.dbdirectory_copy = self.dbdirectory - self.dbpath_copy = self.dbpath # path to db file as in the settings (but not accessable) - buggalo.addExtraData('dbdirectory_copy', self.dbdirectory_copy) - buggalo.addExtraData('dbpath_copy', self.dbpath_copy) - # work in copy directory in the Kodi profile folder - self.dbdirectory = os.path.join(xbmcvfs.translatePath(utils.data_dir()), 'dbcopy' + os.sep) - if not xbmcvfs.exists(self.dbdirectory): # directory has to end with '/' (os.sep) - xbmcvfs.mkdir(self.dbdirectory) - utils.log(u'created directory %s' % str(self.dbdirectory)) - self.dbpath = os.path.join(self.dbdirectory, "watchedlist.db") - if xbmcvfs.exists(self.dbpath_copy): - success = xbmcvfs.copy(self.dbpath_copy, self.dbpath) # copy the external db file to local mirror directory - utils.log(u'copied db file from share: %s -> %s. Success: %d' % (self.dbpath_copy, self.dbpath, success), xbmc.LOGDEBUG) - - buggalo.addExtraData('dbdirectory', self.dbdirectory) - buggalo.addExtraData('dbpath', self.dbpath) - - # connect to the local wl database. create database if it does not exist - self.sqlcon_wl = sqlite3.connect(self.dbpath) - self.sqlcursor_wl = self.sqlcon_wl.cursor() - else: - # MySQL Database on a server - self.sqlcon_wl = mysql.connector.connect(user=utils.getSetting("mysql_user"), password=utils.getSetting("mysql_pass"), database=utils.getSetting("mysql_db"), host=utils.getSetting("mysql_server"), port=utils.getSetting("mysql_port")) - self.sqlcursor_wl = self.sqlcon_wl.cursor() - - # create tables if they don't exist - if int(utils.getSetting("db_format")) != 1: # sqlite file - self.sqlcursor_wl.execute(QUERY_CREATE_MV_SQLITE) - self.sqlcursor_wl.execute(QUERY_CREATE_EP_SQLITE) - self.sqlcursor_wl.execute(QUERY_CREATE_SS_SQLITE) - else: # mysql network database - self.sqlcursor_wl.execute(QUERY_CREATE_MV_MYSQL) - self.sqlcursor_wl.execute(QUERY_CREATE_EP_MYSQL) - self.sqlcursor_wl.execute(QUERY_CREATE_SS_MYSQL) - - # check for dropbox - if DROPBOX_ENABLED and utils.getSetting("dropbox_enabled") == 'true': - # Download Dropbox database only once a day to reduce traffic. - if time.time() > self.downloaded_dropbox_timestamp + 3600.0 * 24.0: - if self.pullFromDropbox(): - return 1 - self.downloaded_dropbox_timestamp = time.time() - # connect to the dropbox wl database. - if self.dropbox_path is not None: - self.sqlcon_db = sqlite3.connect(self.dropbox_path) - self.sqlcursor_db = self.sqlcon_db.cursor() - # create tables in dropbox file, if they don't exist - self.sqlcursor_db.execute(QUERY_CREATE_MV_SQLITE) - self.sqlcursor_db.execute(QUERY_CREATE_EP_SQLITE) - self.sqlcursor_db.execute(QUERY_CREATE_SS_SQLITE) - - buggalo.addExtraData('db_connstatus', 'connected') - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u"Database error while opening %s. '%s'" % (self.dbpath, errstring), xbmc.LOGERROR) - utils.showNotification(utils.getString(32109), errstring, xbmc.LOGERROR) - self.close_db(3) - buggalo.addExtraData('db_connstatus', 'sqlite3 error, closed') - return 1 - except mysql.connector.Error as err: - # Catch common mysql errors and show them to guide the user - utils.log(u"Database error while opening mySQL DB %s [%s:%s@%s]. %s" % (utils.getSetting("mysql_db"), utils.getSetting("mysql_user"), utils.getSetting("mysql_pass"), utils.getSetting("mysql_db"), err.msg), xbmc.LOGERROR) - if err.errno == mysql.connector.errorcode.ER_DBACCESS_DENIED_ERROR: - utils.showNotification(utils.getString(32108), utils.getString(32210) % (utils.getSetting("mysql_user"), utils.getSetting("mysql_db")), xbmc.LOGERROR) - elif err.errno == mysql.connector.errorcode.ER_ACCESS_DENIED_ERROR: - utils.showNotification(utils.getString(32108), utils.getString(32208), xbmc.LOGERROR) - elif err.errno == mysql.connector.errorcode.ER_BAD_DB_ERROR: - utils.showNotification(utils.getString(32108), utils.getString(32209) % utils.getSetting("mysql_db"), xbmc.LOGERROR) - else: - utils.showNotification(utils.getString(32108), err.msg, xbmc.LOGERROR) - buggalo.addExtraData('db_connstatus', 'mysql error, closed') - self.close_db(3) - return 1 - except SystemExit: - return 1 - except BaseException: - utils.log(u"Error while opening %s: %s" % (self.dbpath, sys.exc_info()[2]), xbmc.LOGERROR) - self.close_db(3) - buggalo.addExtraData('dbpath', self.dbpath) - buggalo.addExtraData('db_connstatus', 'error, closed') - buggalo.onExceptionRaised() - return 1 - # only commit the changes if no error occured to ensure database persistence - self.sqlcon_wl.commit() - return 0 - - def close_db(self, flag): - """Close WL database - Argument: - flag - 1 for closing the WatchedList database - 2 for closing the DropBox database - 3 for closing both - - Returns: - return code: - 0 successfully closed database - 1 error - """ - # close watchedlist db (if it is open) - if self.sqlcon_wl and (flag == 1 or flag == 3): - if self.sqlcon_wl: - self.sqlcon_wl.close() - self.sqlcon_wl = 0 - # copy the db file back to the shared directory, if needed - if utils.getSetting("db_format") == '0' and self.dbfileaccess == 'copy': - if xbmcvfs.exists(self.dbpath): - success = xbmcvfs.copy(self.dbpath, self.dbpath_copy) - utils.log(u'copied db file to share: %s -> %s. Success: %d' % (self.dbpath, self.dbpath_copy, success), xbmc.LOGDEBUG) - if not success: - utils.showNotification(utils.getString(32102), utils.getString(32606) % self.dbpath, xbmc.LOGERROR) - return 1 - buggalo.addExtraData('db_connstatus', 'closed') - # close dropbox db - if flag == 2 or flag == 3: - if self.sqlcon_db: - self.sqlcon_db.close() - self.sqlcon_db = 0 - return 0 - - def get_watched_xbmc(self, silent): - """Get Watched States of Kodi Database - - Args: - silent: Do not show notifications if True - - Returns: - return code: - 0 success - 1 error - 4 planned exit (no error) - """ - try: - - ############################################ - # first tv shows with TheTVDB-ID, then tv episodes - if utils.getSetting("w_episodes") == 'true': - ############################################ - # get imdb tv-show id from Kodi database - utils.log(u'get_watched_xbmc: Get all TV shows from Kodi database', xbmc.LOGDEBUG) - json_response = utils.executeJSON({ - "jsonrpc": "2.0", - "method": "VideoLibrary.GetTVShows", - "params": { - "properties": ["title", "uniqueid"], - "sort": {"order": "ascending", "method": "title"} - }, - "id": 1}) - if 'result' in json_response and json_response['result'] is not None and 'tvshows' in json_response['result']: - for item in json_response['result']['tvshows']: - if self.monitor.abortRequested(): - self.close_db(3) - return 4 - tvshowId_xbmc = int(item['tvshowid']) - if not 'uniqueid' in item: - utils.log(u'get_watched_xbmc: tv show "%s" has no field uniqueid in database. tvshowid=%d. Try rescraping.' % (item['title'], tvshowId_xbmc), xbmc.LOGINFO) - continue - try: - tvshowId_tvdb = int(item['uniqueid']['tvdb']) - except BaseException: - utils.log(u'get_watched_xbmc: tv show "%s" has no tvdb-number in database. tvshowid=%d. Unique IDs: %s. Try rescraping.' % (item['title'], tvshowId_xbmc, str(list(item['uniqueid'].keys()))), xbmc.LOGINFO) - if not silent: - utils.showNotification(utils.getString(32101), utils.getString(32297) % (item['title'], tvshowId_xbmc), xbmc.LOGINFO) - tvshowId_tvdb = int(0) - self.tvshows[tvshowId_xbmc] = list([tvshowId_tvdb, item['title']]) - if tvshowId_tvdb > 0: - self.tvshownames[tvshowId_tvdb] = item['title'] - - # Get all watched movies and episodes by unique id from xbmc-database via JSONRPC - self.watchedmovielist_xbmc = list([]) - self.watchedepisodelist_xbmc = list([]) - for modus in ['movie', 'episode']: - if self.monitor.abortRequested(): - self.close_db(3) - return 4 - buggalo.addExtraData('modus', modus) - if modus == 'movie' and utils.getSetting("w_movies") != 'true': - continue - if modus == 'episode' and utils.getSetting("w_episodes") != 'true': - continue - utils.log(u'get_watched_xbmc: Get all %ss from Kodi database' % modus, xbmc.LOGDEBUG) - if modus == 'movie': - # use the JSON-RPC to access the xbmc-database. - json_response = utils.executeJSON({ - "jsonrpc": "2.0", - "method": "VideoLibrary.GetMovies", - "params": { - "properties": ["title", "year", "lastplayed", "playcount", "uniqueid"], - "sort": {"order": "ascending", "method": "title"} - }, - "id": 1 - }) - else: - json_response = utils.executeJSON({ - "jsonrpc": "2.0", - "method": "VideoLibrary.GetEpisodes", - "params": { - "properties": ["tvshowid", "season", "episode", "playcount", "showtitle", "lastplayed", "uniqueid"] - }, - "id": 1 - }) - if modus == 'movie': - searchkey = 'movies' - else: - searchkey = 'episodes' - if 'result' in json_response and json_response['result'] is not None and searchkey in json_response['result']: - # go through all watched movies and save them in the class-variable self.watchedmovielist_xbmc - for item in json_response['result'][searchkey]: - if self.monitor.abortRequested(): - break - if not 'uniqueid' in item: - if modus == 'movie': - utils.log(u'get_watched_xbmc: Movie %s has no field uniqueid in database. Try rescraping.' % (item['title']), xbmc.LOGINFO) - else: # episode - utils.log(u'get_watched_xbmc: Episode id %d (show %d, S%02dE%02d) has no field uniqueid in database. Try rescraping.' % (item['episodeid'], item['tvshowid'], item['season'], item['episode']), xbmc.LOGINFO) - continue - if modus == 'movie': - name = item['title'] + ' (' + str(item['year']) + ')' - try: - # check if movie number is in imdb-format (scraper=imdb) - res = re.compile(r'tt(\d+)').findall(item['uniqueid']['imdb']) - imdbId = int(res[0]) - except BaseException: - # no imdb-number for this movie in database. Skip - utils.log(u'get_watched_xbmc: Movie %s has no imdb-number in database. movieid=%d. IDs are %s. Try rescraping' % (name, int(item['movieid']), str(list(item['uniqueid'].keys()))), xbmc.LOGINFO) - imdbId = 0 - continue - else: # episodes - tvshowId_xbmc = item['tvshowid'] - try: - tvshowName_xbmc = item['showtitle'] - except BaseException: - # TODO: Something is wrong with the database or the json output since the field tvshowid is missing although requested. Check if this error still occurs and remove try-except - utils.log(u'get_watched_xbmc: TV episode id %d (show %d, S%02dE%02d) has no associated showtitle. Skipping.' % (item['episodeid'], item['tvshowid'], item['season'], item['episode']), xbmc.LOGWARNING) - continue - name = '%s S%02dE%02d' % (tvshowName_xbmc, item['season'], item['episode']) - try: - tvshowId_tvdb = self.tvshows[tvshowId_xbmc][0] - except BaseException: - utils.log(u'get_watched_xbmc: Kodi tv showid %d is not in Kodi-table tvshows. Skipping episode id %d (%s)' % (item['tvshowid'], item['episodeid'], name), xbmc.LOGINFO) - continue - if tvshowId_tvdb == 0: - utils.log(u'get_watched_xbmc: tvshow %d has no tvdb-number. Skipping episode id %d (%s)' % (item['tvshowid'], item['episodeid'], name), xbmc.LOGDEBUG) - continue - lastplayed = utils.sqlDateTimeToTimeStamp(item['lastplayed']) # convert to integer-timestamp - playcount = int(item['playcount']) - # add data to the class-variables - if modus == 'movie': - self.watchedmovielist_xbmc.append(list([imdbId, 0, 0, lastplayed, playcount, name, 0, int(item['movieid'])])) # 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid - else: - self.watchedepisodelist_xbmc.append(list([tvshowId_tvdb, int(item['season']), int(item['episode']), lastplayed, playcount, name, 0, int(item['episodeid'])])) # 0tvdbnumber, 1season, 2episode, 3lastplayed, 4playcount, 5name, 6empty, 7episodeid - if not silent: - utils.showNotification(utils.getString(32101), utils.getString(32299) % (len(self.watchedmovielist_xbmc), len(self.watchedepisodelist_xbmc)), xbmc.LOGINFO) - if self.monitor.abortRequested(): - self.close_db(3) - return 4 - return 0 - except SystemExit: - return 4 - except BaseException: - utils.log(u'get_watched_xbmc: error getting the Kodi database : %s' % sys.exc_info()[2], xbmc.LOGERROR) - self.close_db(3) - buggalo.onExceptionRaised() - return 1 - - def get_watched_wl(self, silent): - """Get Watched States of WL Database - - Args: - silent: Do not show notifications if True - - Returns: - return code: - 0 successfully got watched states from WL-database - 1 unknown error (programming related) - 2 shutdown (error in subfunction) - 3 error related to opening the database - 4 exit requested (no error) - """ - - try: - buggalo.addExtraData('self_sqlcursor', self.sqlcursor_wl) - buggalo.addExtraData('self_sqlcon', self.sqlcon_wl) - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - return 2 - - # get watched movies from addon database - self.watchedmovielist_wl = list([]) - if utils.getSetting("w_movies") == 'true': - utils.log(u'get_watched_wl: Get watched movies from WL database', xbmc.LOGDEBUG) - if int(utils.getSetting("db_format")) != 1: # SQLite3 File. Timestamp stored as integer - self.sqlcursor_wl.execute(QUERY_SELECT_MV_SQLITE) - else: # mySQL: Create integer timestamp with the request - self.sqlcursor_wl.execute(QUERY_SELECT_MV_MYSQL) - rows = self.sqlcursor_wl.fetchall() - for row in rows: - if self.monitor.abortRequested(): - self.close_db(1) - return 4 - self.watchedmovielist_wl.append(list([int(row[0]), 0, 0, int(row[1]), int(row[2]), row[3], int(row[4])])) # 0tvdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6lastChange - - # get watched episodes from addon database - self.watchedepisodelist_wl = list([]) - if utils.getSetting("w_episodes") == 'true': - utils.log(u'get_watched_wl: Get watched episodes from WL database', xbmc.LOGDEBUG) - if int(utils.getSetting("db_format")) != 1: # SQLite3 File. Timestamp stored as integer - self.sqlcursor_wl.execute(QUERY_SELECT_EP_SQLITE) - else: # mySQL: Create integer timestamp with the request - self.sqlcursor_wl.execute(QUERY_SELECT_EP_MYSQL) - - rows = self.sqlcursor_wl.fetchall() - for row in rows: - if self.monitor.abortRequested(): - self.close_db(1) - return 4 - try: - name = '%s S%02dE%02d' % (self.tvshownames[int(row[0])], int(row[1]), int(row[2])) - except BaseException: - name = 'tvdb-id %d S%02dE%02d' % (int(row[0]), int(row[1]), int(row[2])) - self.watchedepisodelist_wl.append(list([int(row[0]), int(row[1]), int(row[2]), int(row[3]), int(row[4]), name, int(row[5])])) # 0tvdbnumber, 1season, 2episode, 3lastplayed, 4playcount, 5name, 6lastChange - - if not silent: - utils.showNotification(utils.getString(32101), utils.getString(32298) % (len(self.watchedmovielist_wl), len(self.watchedepisodelist_wl)), xbmc.LOGINFO) - return 0 - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u'get_watched_wl: SQLite Database error getting the wl database. %s' % errstring, xbmc.LOGERROR) - self.close_db(1) - # error could be that the database is locked (for tv show strings). This is not an error to disturb the other functions - return 3 - except mysql.connector.Error as err: - utils.log(u'get_watched_wl: MySQL Database error getting the wl database. %s' % err, xbmc.LOGERROR) - return 3 - except SystemExit: - return 4 - except BaseException: - utils.log(u'get_watched_wl: Error getting the wl database : %s' % sys.exc_info()[2], xbmc.LOGERROR) - self.close_db(1) - buggalo.onExceptionRaised() - return 1 - - def sync_tvshows(self): - """Sync List of TV Shows from Kodi to WL Database - - Returns: - return code: - 0 successfully synched tv shows - 1 database access error - 2 database loading error - 4 exit requested (no error) - """ - - try: - utils.log(u'sync_tvshows: sync tvshows with wl database : %s' % sys.exc_info()[2], xbmc.LOGDEBUG) - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - return 2 - # write eventually new tv shows to wl database - for xbmcid in self.tvshows: - if self.monitor.abortRequested(): - break # all queries will be commited - values = self.tvshows[xbmcid] - if values[0] == 0: - # no imdb-id saved for this tv show. Nothing to process. - continue - if int(utils.getSetting("db_format")) != 1: # sqlite3 - self.sqlcursor_wl.execute(QUERY_INSERT_SS_SQLITE, values) - else: # mysql - self.sqlcursor_wl.execute(QUERY_INSERT_SS_MYSQL, values) - self.database_backup() - self.sqlcon_wl.commit() - if self.monitor.abortRequested(): - self.close_db(1) - return 4 - # get all known tv shows from wl database - self.sqlcursor_wl.execute("SELECT idShow, title FROM tvshows") - rows = self.sqlcursor_wl.fetchall() - for row_i in rows: - if self.monitor.abortRequested(): - break - self.tvshownames[int(row_i[0])] = row_i[1] - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u'sync_tvshows: SQLite Database error accessing the wl database: ''%s''' % errstring, xbmc.LOGERROR) - self.close_db(1) - # error could be that the database is locked (for tv show strings). - return 1 - except mysql.connector.Error as err: - utils.log(u"sync_tvshows: MySQL Database error accessing the wl database: ''%s''" % (err), xbmc.LOGERROR) - self.close_db(1) - return 1 - except SystemExit: - return 4 - except BaseException: - utils.log(u'sync_tvshows: Error getting the wl database: ''%s''' % sys.exc_info()[2], xbmc.LOGERROR) - self.close_db(1) - buggalo.onExceptionRaised() - return 1 - if self.monitor.abortRequested(): - return 4 - return 0 - - def write_wl_wdata(self): - """Go through all watched movies from Kodi and check whether they are up to date in the addon database - - Returns: - return code: - 0 successfully written WL - 1 program exception - 2 database loading error - 4 SystemExit - """ - - buggalo.addExtraData('self_sqlcursor', self.sqlcursor_wl) - buggalo.addExtraData('self_sqlcon', self.sqlcon_wl) - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - return 2 - for modus in ['movie', 'episode']: - buggalo.addExtraData('modus', modus) - if modus == 'movie' and utils.getSetting("w_movies") != 'true': - continue - if modus == 'episode' and utils.getSetting("w_episodes") != 'true': - continue - utils.log(u'write_wl_wdata: Write watched %ss to WL database' % modus, xbmc.LOGDEBUG) - count_insert = 0 - count_update = 0 - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS = xbmcgui.DialogProgress() - DIALOG_PROGRESS.create(utils.getString(32101), utils.getString(32105)) - if modus == 'movie': - list_length = len(self.watchedmovielist_xbmc) - else: - list_length = len(self.watchedepisodelist_xbmc) - - for i in range(list_length): - if self.monitor.abortRequested(): - break # this loop can take some time in debug mode and prevents Kodi to exit - if utils.getSetting("progressdialog") == 'true' and DIALOG_PROGRESS.iscanceled(): - if modus == 'movie': - strno = 32202 - else: - strno = 32203 - utils.showNotification(utils.getString(strno), utils.getString(32301) % (count_insert, count_update), xbmc.LOGINFO) - return 2 - if modus == 'movie': - row_xbmc = self.watchedmovielist_xbmc[i] - else: - row_xbmc = self.watchedepisodelist_xbmc[i] - - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.update(int(100 * (i + 1) / list_length), (utils.getString(32105)+"\n"+utils.getString(32610) % (i + 1, list_length, row_xbmc[5]))) - - try: - res = self._wl_update_media(modus, row_xbmc, 0, 0, 0) - count_insert += res['num_new'] - count_update += res['num_update'] - - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u'write_wl_wdata: SQLite Database error ''%s'' while updating %s %s' % (errstring, modus, row_xbmc[5]), xbmc.LOGERROR) - # error at this place is the result of duplicate movies, which produces a DUPLICATE PRIMARY KEY ERROR - return 1 - except mysql.connector.Error as err: - utils.log(u'write_wl_wdata: MySQL Database error ''%s'' while updating %s %s' % (err, modus, row_xbmc[5]), xbmc.LOGERROR) - self.close_db(1) - return 1 # error while writing. Do not continue with episodes, if movies raised an exception - except SystemExit: - return 4 - except BaseException: - utils.log(u'write_wl_wdata: Error while updating %s %s: %s' % (modus, row_xbmc[5], sys.exc_info()[2]), xbmc.LOGERROR) - self.close_db(1) - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - buggalo.addExtraData('count_update', count_update) - buggalo.addExtraData('count_insert', count_insert) - buggalo.onExceptionRaised() - return 1 - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - if self.monitor.abortRequested(): - break # break outer loop if exit requested. Do not do any database operations below - # only commit the changes if no error occured to ensure database persistence - if count_insert > 0 or count_update > 0: - self.database_backup() - self.sqlcon_wl.commit() - if modus == 'movie': - strno = [32202, 32301] - else: - strno = [32203, 32301] - utils.showNotification(utils.getString(strno[0]), utils.getString(strno[1]) % (count_insert, count_update), xbmc.LOGINFO) - return 0 - - def write_xbmc_wdata(self, progressdialogue, notifications): - """Go through all watched movies/episodes from the wl-database and check, - if the xbmc-database is up to date - - Args: - progressdialogue: Show Progress Bar if True - notifications: 0= no, 1=only changed info, 2=all - - Returns: - return code: - 0 successfully written Kodi database - 1 program exception - 2 cancel by user interaction - 4 SystemExit - """ - - for modus in ['movie', 'episode']: - buggalo.addExtraData('modus', modus) - if modus == 'movie' and utils.getSetting("w_movies") != 'true': - continue - if modus == 'episode' and utils.getSetting("w_episodes") != 'true': - continue - - utils.log(u'write_xbmc_wdata: Write watched %ss to Kodi database (pd=%d, noti=%d)' % (modus, progressdialogue, notifications), xbmc.LOGDEBUG) - count_update = 0 - if progressdialogue: - DIALOG_PROGRESS = xbmcgui.DialogProgress() - DIALOG_PROGRESS.create(utils.getString(32101), utils.getString(32106)) - - # list to iterate over - if modus == 'movie': - list_length = len(self.watchedmovielist_wl) - else: - list_length = len(self.watchedepisodelist_wl) - # iterate over wl-list - for j in range(list_length): - if self.monitor.abortRequested(): - break # this loop can take some time in debug mode and prevents Kodi to exit - if progressdialogue and DIALOG_PROGRESS.iscanceled(): - if notifications > 0: - utils.showNotification(utils.getString(32204), utils.getString(32302) % (count_update), xbmc.LOGINFO) - return 2 - # get media-specific list items - if modus == 'movie': - row_wl = self.watchedmovielist_wl[j] - else: - row_wl = self.watchedepisodelist_wl[j] - season = row_wl[1] - episode = row_wl[2] - imdbId = row_wl[0] - name = row_wl[5] - - if progressdialogue: - DIALOG_PROGRESS.update(int(100 * (j + 1) / list_length), (utils.getString(32106)+"\n"+utils.getString(32610) % (j + 1, list_length, name))) - try: - # search the unique movie/episode id in the xbmc-list - if modus == 'movie': - indices = [i for i, x in enumerate(self.watchedmovielist_xbmc) if x[0] == imdbId] # the movie can have multiple entries in xbmc - else: - indices = [i for i, x in enumerate(self.watchedepisodelist_xbmc) if x[0] == imdbId and x[1] == season and x[2] == episode] - lastplayed_wl = row_wl[3] - playcount_wl = row_wl[4] - lastchange_wl = row_wl[6] - if indices: - # the movie/episode is already in the xbmc-list - for i in indices: - if modus == 'movie': - row_xbmc = self.watchedmovielist_xbmc[i] - else: - row_xbmc = self.watchedepisodelist_xbmc[i] - - lastplayed_xbmc = row_xbmc[3] - playcount_xbmc = row_xbmc[4] - - change_xbmc_db = False - # check if movie/episode is set as unwatched in the wl database - if playcount_wl != playcount_xbmc and lastchange_wl > lastplayed_xbmc: - change_xbmc_db = True - # compare playcount and lastplayed (update if Kodi data is older) - if playcount_xbmc < playcount_wl or (lastplayed_xbmc < lastplayed_wl and playcount_wl > 0): - change_xbmc_db = True - if not change_xbmc_db: - # utils.log(u'write_xbmc_wdata: Kodi database up-to-date for tt%d, %s' % (imdbId, row_xbmc[2]), xbmc.LOGDEBUG) - continue - # check if the lastplayed-timestamp in wl is useful - if playcount_wl == 0: - lastplayed_new = 0 - else: - if lastplayed_wl == 0: - lastplayed_new = lastplayed_xbmc - else: - lastplayed_new = lastplayed_wl - # update database - mediaid = row_xbmc[7] - if modus == 'movie': - jsonmethod = "VideoLibrary.SetMovieDetails" - idfieldname = "movieid" - else: - jsonmethod = "VideoLibrary.SetEpisodeDetails" - idfieldname = "episodeid" - jsondict = { - "jsonrpc": "2.0", - "method": jsonmethod, - "params": {idfieldname: mediaid, "playcount": playcount_wl, "lastplayed": utils.TimeStamptosqlDateTime(lastplayed_new)}, - "id": 1 - } - json_response = utils.executeJSON(jsondict) - if 'result' in json_response and json_response['result'] == 'OK': - utils.log(u'write_xbmc_wdata: Kodi database updated for %s. playcount: {%d -> %d}, lastplayed: {"%s" -> "%s"} (%sid=%d)' % (name, playcount_xbmc, playcount_wl, utils.TimeStamptosqlDateTime(lastplayed_xbmc), utils.TimeStamptosqlDateTime(lastplayed_new), modus, mediaid), xbmc.LOGINFO) - if playcount_wl == 0: - if notifications > 0: - utils.showNotification(utils.getString(32404), name, xbmc.LOGDEBUG) - else: - if notifications > 0: - utils.showNotification(utils.getString(32401), name, xbmc.LOGDEBUG) - count_update += 1 - # update the xbmc-db-mirror-variable - if modus == 'movie': - self.watchedmovielist_xbmc[i][3] = lastplayed_new - self.watchedmovielist_xbmc[i][4] = playcount_wl - else: - self.watchedepisodelist_xbmc[i][3] = lastplayed_new - self.watchedepisodelist_xbmc[i][4] = playcount_wl - else: - utils.log(u'write_xbmc_wdata: error updating Kodi database. %s. json_response=%s' % (name, str(json_response)), xbmc.LOGERROR) - - else: - # the movie is in the watched-list but not in the xbmc-list -> no action - # utils.log(u'write_xbmc_wdata: movie not in Kodi database: tt%d, %s' % (imdbId, row_xbmc[2]), xbmc.LOGDEBUG) - continue - except SystemExit: - return 4 - except BaseException: - utils.log(u"write_xbmc_wdata: Error while updating %s %s: %s" % (modus, name, sys.exc_info()[2]), xbmc.LOGERROR) - if progressdialogue: - DIALOG_PROGRESS.close() - buggalo.addExtraData('count_update', count_update) - buggalo.onExceptionRaised() - return 1 - - if progressdialogue: - DIALOG_PROGRESS.close() - if self.monitor.abortRequested(): - break # break the outer loop (movie/episode if exit requested) - if notifications > 1: - if modus == 'movie': - strno = [32204, 32302] - else: - strno = [32205, 32303] - utils.showNotification(utils.getString(strno[0]), utils.getString(strno[1]) % (count_update), xbmc.LOGINFO) - return 0 - - def database_backup(self): - """create a copy of the database, in case something goes wrong (only if database file is used) - - Returns: - return code: - 0 successfully copied database - 1 file writing error - 2 program exception - 4 SystemExit - """ - - if utils.getSetting("db_format") != '0': - return 0 # no backup needed since we are using mysql database - - if utils.getSetting('dbbackupcount') == '0': - return 0 # no backup requested in the addon settings - - if not self.dbbackupdone: - if not xbmcvfs.exists(self.dbpath): - utils.log(u'database_backup: directory %s does not exist. No backup possible.' % self.dbpath, xbmc.LOGERROR) - return 1 - now = datetime.datetime.now() - timestr = u'%04d%02d%02d_%02d%02d%02d' % (now.year, now.month, now.day, now.hour, now.minute, now.second) - zipfilename = os.path.join(self.dbdirectory, timestr + u'-watchedlist.db.zip') - try: - with zipfile.ZipFile(zipfilename, 'w') as zf: - zf.write(self.dbpath, arcname='watchedlist.db', compress_type=zipfile.ZIP_DEFLATED) - self.dbbackupdone = True - utils.log(u'database_backup: database backup copy created to %s' % zipfilename, xbmc.LOGINFO) - # copy the zip file with Kodi file system, if needed - if self.dbfileaccess == 'copy': - xbmcvfs.copy(zipfilename, os.path.join(self.dbdirectory_copy, timestr + u'-watchedlist.db.zip')) - xbmcvfs.delete(zipfilename) - except ValueError as err: # e.g. "timestamps before 1980" - utils.showNotification(utils.getString(32102), utils.getString(32608) % str(err), xbmc.LOGERROR) - utils.log(u'database_backup: Error creating database backup %s: %s' % (zipfilename, str(err)), xbmc.LOGERROR) - self.dbbackupdone = True # pretend this was done to avoid log spamming - return 2 - except IOError as err: # e.g. "permission denied" - utils.showNotification(utils.getString(32102), utils.getString(32608) % err.strerror, xbmc.LOGERROR) - utils.log(u'database_backup: Error creating database backup %s: (%s) %s' % (zipfilename, err.errno, err.strerror), xbmc.LOGERROR) - self.dbbackupdone = True # pretend this was done to avoid log spamming - return 2 - except SystemExit: - return 4 - except BaseException: - buggalo.addExtraData('zipfilename', zipfilename) - buggalo.onExceptionRaised() - return 2 - return 0 - - def database_backup_delete(self): - """delete old backup files - - Returns: - return code: - 0 no operation done or operation successful - 1 error deleting the backups - """ - - if not self.dbbackupdone: - return 0 - # Limit number of backup files to the specified value - backupsize = int(utils.getSetting('dbbackupcount')) - if backupsize == -1: - return 0 # do not delete old backup files - if self.dbfileaccess == 'copy': - # Database is on a network share. The backup is also copied to that location. Delete old backups there. - backupdir = self.dbdirectory_copy - else: - # Database and backups are in the default folders - backupdir = self.dbdirectory - - files = xbmcvfs.listdir(backupdir)[1] - # find database copy files among all files in that directory - files_match = [] - for i, f in enumerate(files): - if self.monitor.abortRequested(): - break - if re.match(r'\d+_\d+-watchedlist\.db\.zip', f) is not None: # match the filename string from database_backup() - files_match.append(f) - files_match = sorted(files_match, reverse=True) - # Iterate over backup files starting with newest. Delete oldest - for i, f in enumerate(files_match): - if self.monitor.abortRequested(): - break - if i >= int(utils.getSetting('dbbackupcount')): - # delete file - utils.log(u'database_backup_delete: Delete old backup file %d/%d (%s)' % (i + 1, len(files_match), f), xbmc.LOGINFO) - try: - xbmcvfs.delete(os.path.join(backupdir, f)) - except BaseException: - utils.log(u'database_backup_delete: Error deleting old backup file %d (%s)' % (i, os.path.join(backupdir, f)), xbmc.LOGERROR) - return 1 - return 0 - - def watch_user_changes(self, idletime_old, idletime): - """check if the user made changes in the watched states. Especially setting movies as "not watched". - This can not be recognized by the other functions - - Args: - idletime_old: Old Idle Time - idletime: New Idle Time - - Returns: - return code: - 0 no operation done - 1 operation successful - 2 database error - 4 system exit (no error) - """ - - if xbmc.Player().isPlaying(): - return 0 - if idletime > idletime_old: - # the idle time increased. No user interaction probably happened - return 0 - self.watch_user_changes_count = self.watch_user_changes_count + 1 - utils.log(u'watch_user_changes: Check for user changes (no. %d)' % self.watch_user_changes_count, xbmc.LOGDEBUG) - - # save previous state - old_watchedmovielist_xbmc = self.watchedmovielist_xbmc - old_watchedepisodelist_xbmc = self.watchedepisodelist_xbmc - # get new state - self.get_watched_xbmc(1) - # save exception information - buggalo.addExtraData('len_old_watchedmovielist_xbmc', len(old_watchedmovielist_xbmc)) - buggalo.addExtraData('len_old_watchedepisodelist_xbmc', len(old_watchedepisodelist_xbmc)) - buggalo.addExtraData('len_self_watchedmovielist_xbmc', len(self.watchedmovielist_xbmc)) - buggalo.addExtraData('len_self_watchedepisodelist_xbmc', len(self.watchedepisodelist_xbmc)) - # Separate the change detection and the change in the database to - # prevent circle reference: Two steps for watching user changes - # First step: Compare states of movies/episodes - for modus in ['movie', 'episode']: - if self.monitor.abortRequested(): - return 4 - buggalo.addExtraData('modus', modus) - if modus == 'movie' and utils.getSetting("w_movies") != 'true': - continue - if modus == 'episode' and utils.getSetting("w_episodes") != 'true': - continue - if modus == 'movie': - list_new = self.watchedmovielist_xbmc - list_old = old_watchedmovielist_xbmc - else: - list_new = self.watchedepisodelist_xbmc - list_old = old_watchedepisodelist_xbmc - if not list_old or not list_new: - # one of the lists is empty: nothing to compare. No user changes noticable - continue - indices_changed = list([]) # list for corresponding new/old indices - for i_n, row_xbmc in enumerate(list_new): - if self.monitor.abortRequested(): - return 4 - mediaid = row_xbmc[7] - lastplayed_new = row_xbmc[3] - playcount_new = row_xbmc[4] - # index of this movie/episode in the old database (before the change by the user) - if (len(list_old) > i_n) and (list_old[i_n][7] == mediaid): - i_o = i_n # db did not change - else: # search the movieid - i_o = [i for i, x in enumerate(list_old) if x[7] == mediaid] - if not i_o: - continue # movie is not in old array - i_o = i_o[0] # convert list to int - lastplayed_old = list_old[i_o][3] - playcount_old = list_old[i_o][4] - - if playcount_new != playcount_old or lastplayed_new != lastplayed_old: - if playcount_new == playcount_old and playcount_new == 0: - continue # do not add lastplayed to database, when playcount = 0 - # The user changed the playcount or lastplayed. - # update wl with new watched state - indices_changed.append([i_n, i_o]) - - # Second step: Go through all movies changed by the user - for icx in indices_changed: - if self.monitor.abortRequested(): - return 4 - i_o = icx[1] - i_n = icx[0] - row_xbmc = list_new[i_n] - row_xbmc_old = list_old[i_o] - lastplayed_old = row_xbmc_old[3] - playcount_old = row_xbmc_old[4] - lastplayed_new = row_xbmc[3] - playcount_new = row_xbmc[4] - mediaid = row_xbmc[7] - utils.log(u'watch_user_changes: %s "%s" changed playcount {%d -> %d} lastplayed {"%s" -> "%s"}. %sid=%d' % (modus, row_xbmc[5], playcount_old, playcount_new, utils.TimeStamptosqlDateTime(lastplayed_old), utils.TimeStamptosqlDateTime(lastplayed_new), modus, mediaid)) - try: - self._wl_update_media(modus, row_xbmc, 1, 1, 0) - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u'watch_user_changes: SQLite Database error (%s) while updating %s %s' % (errstring, modus, row_xbmc[5])) - utils.showNotification(utils.getString(32102), utils.getString(32606) % ('(%s)' % errstring), xbmc.LOGERROR) - # error because of db locked or similar error - self.close_db(1) - return 2 - except mysql.connector.Error as err: - # Catch common mysql errors and show them to guide the user - utils.log(u'watch_user_changes: MySQL Database error (%s) while updating %s %s' % (err, modus, row_xbmc[5])) - utils.showNotification(utils.getString(32102), utils.getString(32606) % ('(%s)' % err), xbmc.LOGERROR) - self.close_db(1) - return 2 - - # update Kodi watched status, e.g. to set duplicate movies also as watched - if indices_changed: - self.write_xbmc_wdata(0, 1) # this changes self.watchedmovielist_xbmc - return 1 - - def _wl_update_media(self, mediatype, row_xbmc, saveanyway, commit, lastChange): - """update the wl database for one movie/episode with the information in row_xbmc. - - This function is intended to be private and should not be called externally - - Args: - mediatype: 'episode' or 'movie' - row_xbmc: One row of the Kodi media table self.watchedmovielist_xbmc. - saveanyway: Skip checks whether not to save the changes - commit: The db change is committed directly (slow with many movies, but safe) - lastChange: Last change timestamp of the given data. - If 0: Data from kodi. - If >0: Data from other watchedlist database for merging - - Returns: Dict with fields - errcode: - 0 No errors - 2 error loading the database - num_new, num_update: - Number of new and updated entries - """ - retval = {'errcode': 0, 'num_new': 0, 'num_update': 0} - buggalo.addExtraData('self_sqlcursor', self.sqlcursor_wl) - buggalo.addExtraData('self_sqlcon', self.sqlcon_wl) - buggalo.addExtraData('len_self_watchedmovielist_wl', len(self.watchedmovielist_wl)) - buggalo.addExtraData('len_self_watchedepisodelist_wl', len(self.watchedepisodelist_wl)) - buggalo.addExtraData('len_self_tvshownames', len(self.tvshownames)) - buggalo.addExtraData('row_xbmc', row_xbmc) - buggalo.addExtraData('saveanyway', saveanyway) - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - retval['errcode'] = 2 - return retval - - buggalo.addExtraData('modus', mediatype) - # row_xbmc: 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid - imdbId = row_xbmc[0] - lastplayed_xbmc = row_xbmc[3] - playcount_xbmc = row_xbmc[4] - name = row_xbmc[5] - if mediatype == 'episode': - season = row_xbmc[1] - episode = row_xbmc[2] - - self.database_backup() - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - retval['errcode'] = 2 - return retval - if not saveanyway and playcount_xbmc == 0 and lastChange == 0: - # playcount in xbmc-list is empty. Nothing to save - # utils.log(u'wl_update_%s: not watched in xbmc: tt%d, %s' % (modus, imdbId, name), xbmc.LOGDEBUG) - return retval - if lastChange == 0: - lastchange_new = int(time.time()) - else: # data from WL database with given last change timestamp - lastchange_new = lastChange - if lastplayed_xbmc == -1: # used for external calls - lastplayed_xbmc = int(time.time()) - if mediatype == 'movie': - j = [ii for ii, x in enumerate(self.watchedmovielist_wl) if x[0] == imdbId] - else: # if mediatype == 'episode': - j = [ii for ii, x in enumerate(self.watchedepisodelist_wl) if x[0] == imdbId and x[1] == season and x[2] == episode] - if j: # j is the list of indexes of the occurences of the given row - j = j[0] # there can only be one valid index j, since only one entry in wl per imdbId - # the movie is already in the watched-list - if mediatype == 'movie': - row_wl = self.watchedmovielist_wl[j] - else: - row_wl = self.watchedepisodelist_wl[j] - lastplayed_wl = row_wl[3] - playcount_wl = row_wl[4] - lastchange_wl = row_wl[6] - - if not saveanyway: - # check if an update of the wl database is necessary (xbmc watched status newer) - if lastChange == 0: # information from xbmc: Criterion Playcount and lastplayed-timestamp - if lastchange_wl > lastplayed_xbmc: - return retval # no update of WL-db. Return - if playcount_wl >= playcount_xbmc and lastplayed_wl >= lastplayed_xbmc: - return retval # everything up-to-date - elif lastChange <= lastchange_wl: # information from watchedlist. Only criterion: lastChange-timestamp - return retval - - # check if the lastplayed-timestamp in Kodi is useful - if lastplayed_xbmc == 0 and lastChange == 0: - lastplayed_new = lastplayed_wl - else: - lastplayed_new = lastplayed_xbmc - else: - lastplayed_new = lastplayed_xbmc - - if mediatype == 'movie': - if int(utils.getSetting("db_format")) != 1: # sqlite3 - sql = QUERY_MV_UPDATE_SQLITE - else: # mysql - sql = QUERY_MV_UPDATE_MYSQL - values = list([playcount_xbmc, lastplayed_new, lastchange_new, imdbId]) - else: - if int(utils.getSetting("db_format")) != 1: # sqlite3 - sql = QUERY_EP_UPDATE_SQLITE - else: # mysql - sql = QUERY_EP_UPDATE_MYSQL - values = list([playcount_xbmc, lastplayed_new, lastchange_new, imdbId, season, episode]) - self.sqlcursor_wl.execute(sql, values) - retval['num_update'] = self.sqlcursor_wl.rowcount - # update the local mirror variable of the wl database: # 0imdbnumber, season, episode, 3lastPlayed, 4playCount, 5title, 6lastChange - if mediatype == 'movie': - self.watchedmovielist_wl[j] = list([imdbId, 0, 0, lastplayed_new, playcount_xbmc, name, lastchange_new]) - else: - self.watchedepisodelist_wl[j] = list([imdbId, season, episode, lastplayed_new, playcount_xbmc, name, lastchange_new]) - utils.log(u'wl_update_%s: updated wl db for "%s" (id %d). playcount: {%d -> %d}. lastplayed: {"%s" -> "%s"}. lastchange: "%s. Affected %d row."' % (mediatype, name, imdbId, playcount_wl, playcount_xbmc, utils.TimeStamptosqlDateTime(lastplayed_wl), utils.TimeStamptosqlDateTime(lastplayed_new), utils.TimeStamptosqlDateTime(lastchange_new), self.sqlcursor_wl.rowcount), xbmc.LOGDEBUG) - if playcount_xbmc > 0: - utils.showNotification(utils.getString(32403), name, xbmc.LOGDEBUG) - else: - utils.showNotification(utils.getString(32405), name, xbmc.LOGDEBUG) - else: - # the movie is not in the watched-list -> insert the movie - # order: idMovieImdb,playCount,lastChange,lastPlayed,title - if mediatype == 'movie': - if int(utils.getSetting("db_format")) != 1: # sqlite3 - sql = QUERY_MV_INSERT_SQLITE - else: # mysql - sql = QUERY_MV_INSERT_MYSQL - values = list([imdbId, playcount_xbmc, lastchange_new, lastplayed_xbmc, name]) - else: # episode - if int(utils.getSetting("db_format")) != 1: # sqlite3 - sql = QUERY_EP_INSERT_SQLITE - else: # mysql - sql = QUERY_EP_INSERT_MYSQL - values = list([imdbId, season, episode, playcount_xbmc, lastchange_new, lastplayed_xbmc]) - self.sqlcursor_wl.execute(sql, values) - utils.log(u'wl_update_%s: new entry for wl database: "%s", lastChange="%s", lastPlayed="%s", playCount=%d. Affected %d row.' % (mediatype, name, utils.TimeStamptosqlDateTime(lastchange_new), utils.TimeStamptosqlDateTime(lastplayed_xbmc), playcount_xbmc, self.sqlcursor_wl.rowcount)) - retval['num_new'] = self.sqlcursor_wl.rowcount - # update the local mirror variable of the wl database - if mediatype == 'movie': - self.watchedmovielist_wl.append(list([imdbId, 0, 0, lastplayed_xbmc, playcount_xbmc, name, lastchange_new])) - else: - self.watchedepisodelist_wl.append(list([imdbId, season, episode, lastplayed_xbmc, playcount_xbmc, name, lastchange_new])) - if playcount_xbmc > 0: - utils.showNotification(utils.getString(32402), name, xbmc.LOGDEBUG) - else: - utils.showNotification(utils.getString(32405), name, xbmc.LOGDEBUG) - if commit: - self.sqlcon_wl.commit() - return retval - - def wl_update_media(self, mediatype, row_xbmc, saveanyway, commit, lastChange): - """update the wl database for one movie/episode with the information in row_xbmc. - - Function for external access with basic error handling - - Args: - mediatype: 'episode' or 'movie' - row_xbmc: One row of the Kodi media table self.watchedmovielist_xbmc or self.watchedepisodelist_xbmc. - List with [0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid] - or [0tvdbnumber, 1season, 2episode, 3lastplayed, 4playcount, 5name, 6empty, 7episodeid] - saveanyway: Skip checks whether not to save the changes - commit: The db change is committed directly (slow with many movies, but safe) - lastChange: Last change timestamp of the given data. - If 0: Data from kodi. - If >0: Data from other watchedlist database for merging - - Returns: Dict with fields - errcode: - 0 No errors - 1 error with the database - 2 error loading the database - num_new, num_update: - Number of new and updated entries - """ - retval = {'errcode': 0, 'num_new': 0, 'num_update': 0} - try: - return self._wl_update_media(mediatype, row_xbmc, saveanyway, commit, lastChange) - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u'wl_update_media: SQLite Database error accessing the wl database: ''%s''' % errstring, xbmc.LOGERROR) - utils.showNotification(utils.getString(32109), errstring, xbmc.LOGERROR) - self.close_db(1) - retval['errcode'] = 1 - except mysql.connector.Error as err: - utils.log(u"wl_update_media: MySQL Database error accessing the wl database: ''%s''" % (err.msg), xbmc.LOGERROR) - utils.showNotification(utils.getString(32108), err.msg, xbmc.LOGERROR) - self.close_db(1) - retval['errcode'] = 1 - return retval - - def merge_dropbox_local(self): - """ - Merge the remote (eg: Dropbox) database into the local one - The resulting merged database contains all watched movies and episodes of both databases - - Returns: - return code: - 0 successfully synched databases - 1 database access error - 2 database loading error - 4 exit requested (no error) - """ - - if not self.dropbox_path: - utils.log(u'merge_dropbox_local: no dropbox path -- assuming download failed. not merging remote database.') - return 1 - - try: - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - return 2 - - # do not merge tvshows between dropbox and local wl database. - # Only movies and episodes - count_insert = 0 - count_update = 0 - for mediatype in ['movie', 'episode']: - # strno: number of string for heading of notifications - # sql_select_dropbox: Definitions of SQL queries to get data from the dropbox database - if mediatype == 'movie': - strno = 32711 - sql_select_dropbox = QUERY_SELECT_MV_SQLITE - else: - strno = 32712 - sql_select_dropbox = QUERY_SELECT_EP_SQLITE - utils.log(u'wl_merge_dropbox_local (%s): Start merging the remote into the local database' % mediatype) - self.sqlcursor_db.execute(sql_select_dropbox) - # loop through all rows of the remote (dropbox) database and merge it into the local database with - # with the function for merging the Kodi database - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS = xbmcgui.DialogProgress() - DIALOG_PROGRESS.create(utils.getString(strno), utils.getString(32105)) - rows = self.sqlcursor_db.fetchall() - list_length = len(rows) - for i in range(list_length): - if self.monitor.abortRequested(): - break - row = rows[i] # see definition of sql_select_dropbox for contents of `row` - if mediatype == 'movie': # idMovieImdb, 1lastPlayed, 2playCount, 3title, 4lastChange - name = "%s" % row[3] - playCount = row[2] - lastChange = row[4] - lastPlayed = row[1] - else: # 0idShow, 1season, 2episode, 3lastPlayed, 4playCount, 5lastChange - try: - name = '%s S%02dE%02d' % (self.tvshownames[int(row[0])], int(row[1]), int(row[2])) - except BaseException: - name = 'tvdb-id %d S%02dE%02d' % (int(row[0]), int(row[1]), int(row[2])) - playCount = row[4] - lastChange = row[5] - lastPlayed = row[3] - - # handle the row from the dropbox database as if it came from the Kodi database and - # store it in the local WL database (same function call) - # row_xbmc_sim: 0imdbnumber, 1seasonnumber, 2episodenumber, 3lastPlayed, 4playCount, 5title, 6empty, 7movieid - row_xbmc_sim = [0, 0, 0, lastPlayed, playCount, name, 0] - if mediatype == 'movie': - row_xbmc_sim[0] = row[0] - else: - row_xbmc_sim[0:3] = row[0:3] - res = self._wl_update_media(mediatype, row_xbmc_sim, 0, 0, lastChange) - count_insert += res['num_new'] - count_update += res['num_update'] - - # check if update is canceled - if utils.getSetting("progressdialog") == 'true' and DIALOG_PROGRESS.iscanceled(): - return 2 - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.update(int(100 * (i + 1) / list_length), (utils.getString(32105)+"\n"+utils.getString(32610) % (i + 1, list_length, row_xbmc_sim[5]))) - - utils.showNotification(utils.getString(strno), utils.getString(32301) % (count_insert, count_update), xbmc.LOGINFO) - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - if self.monitor.abortRequested(): - return 4 - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u'merge_dropbox_local: SQLite Database error accessing the wl database: ''%s''' % errstring, xbmc.LOGERROR) - self.close_db(3) - # error could be that the database is locked (for tv show strings). - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - return 1 - except SystemExit: - return 4 - except BaseException: - utils.log(u'merge_dropbox_local: Error getting the wl database: ''%s''' % sys.exc_info()[2], xbmc.LOGERROR) - self.close_db(3) - buggalo.onExceptionRaised() - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - return 1 - # commit all changes: This saves the database file physically - self.sqlcon_wl.commit() - return 0 - - def merge_local_dropbox(self): - """ - Merge the local database into the remote one (e.g. dropbox) by copying all tables directly - the content is already merged after loading the remote database (merge_dropbox_local) - - Returns: - return code: - 0 successfully synched databases - 1 database access error - 2 database loading error - 4 exit requested (no error) - """ - - if not self.dropbox_path: - utils.log(u'merge_dropbox: no dropbox path -- assuming download failed. not merging remote database.') - return 1 - - try: - if self.sqlcursor_wl == 0 or self.sqlcon_wl == 0: - if self.load_db(): - return 2 - - # do not merge tvshows between dropbox and local wl database - - # clear dropbox database and just insert all rows from the local db - self.sqlcursor_db.execute(QUERY_CLEAR_MV_SQLITE) - self.sqlcursor_db.execute(QUERY_CLEAR_EP_SQLITE) - - for mediatype in ['movie', 'episode']: - # strno: number of string for heading of notifications - # sql_select_wl: Definitions of SQL queries to get data from the local database - utils.log(u'merge_local_dropbox (%s): Start merging the local into the remote database' % mediatype) - if mediatype == 'movie': - strno = 32718 - rows = self.watchedmovielist_wl # 0imdbnumber, 1empty, 2empty, 3lastPlayed, 4playCount, 5title, 6lastChange - else: - strno = 32719 - rows = self.watchedepisodelist_wl # 0tvdbnumber, 1season, 2episode, 3lastplayed, 4playcount, 5empty, 6lastChange - - # loop through all rows of the local database and merge it into the remote (dropbox) database - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS = xbmcgui.DialogProgress() - DIALOG_PROGRESS.create(utils.getString(strno), utils.getString(32716)) - list_length = len(rows) - for i in range(list_length): - if self.monitor.abortRequested(): - break - row = rows[i] # see definition of queries for contents of `row` - if mediatype == 'movie': - name = row[5] - sql = QUERY_MV_INSERT_SQLITE # idMovieImdb,playCount,lastChange,lastPlayed,title - values = list([row[0], row[4], row[6], row[3], row[5]]) - else: - try: - name = '%s S%02dE%02d' % (self.tvshownames[int(row[0])], int(row[1]), int(row[2])) - except BaseException: - name = 'tvdb-id %d S%02dE%02d' % (int(row[0]), int(row[1]), int(row[2])) - sql = QUERY_EP_INSERT_SQLITE # idShow,season,episode,playCount,lastChange,lastPlayed - values = list([row[0], row[1], row[2], row[4], row[6], row[3]]) - self.sqlcursor_db.execute(sql, values) - - # check if update is canceled DIALOG_PROGRESS.close() - if utils.getSetting("progressdialog") == 'true' and DIALOG_PROGRESS.iscanceled(): - return 2 - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.update(int(100 * (i + 1) / list_length), (utils.getString(32716)+"\n"+utils.getString(32610) % (i + 1, list_length, name))) - utils.showNotification(utils.getString(strno), (utils.getString(32717)) % list_length, xbmc.LOGINFO) - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - if self.monitor.abortRequested(): - return 4 # no further database operations - except sqlite3.Error as err: - try: - errstring = err.args[0] # TODO: Find out, why this does not work some times - except BaseException: - errstring = '' - utils.log(u'merge_local_dropbox: SQLite Database error accessing the wl database: ''%s''' % errstring, xbmc.LOGERROR) - self.close_db(3) - # error could be that the database is locked (for tv show strings). - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - return 1 - except SystemExit: - return 4 - except BaseException: - utils.log(u'merge_local_dropbox: Error getting the wl database: ''%s''' % sys.exc_info()[2], xbmc.LOGERROR) - self.close_db(3) - buggalo.onExceptionRaised() - if utils.getSetting("progressdialog") == 'true': - DIALOG_PROGRESS.close() - return 1 - # only commit the changes if no error occured above (to ensure database persistence) - self.sqlcon_db.commit() - self.close_db(2) # close dropbox database - return 0 - - def pushToDropbox(self): - dropbox_key = utils.getSetting('dropbox_apikey') - - #utils.showNotification(utils.getString(32204), utils.getString(32302)%(count_update)) - # feign success if there is no local dropbox database file - if not (self.dropbox_path and dropbox_key): - return - - dest_file = '/' + 'watchedlist.db' - old_file = '/old' + 'watchedlist.db' - - client = dropbox.Dropbox(dropbox_key) - - # delete the old watched list. Failure here doesn't really matter - try: - client.files_delete(old_file) - except BaseException: - utils.log(u'Dropbox error: Unable to delete previous old watched list (%s)' % old_file) - - # rename the previously uploaded watchlist to "oldWHATEVER" - try: - client.files_move_v2(dest_file, old_file) - except BaseException: - utils.log(u'Dropbox error: Unable rename previous watched list') - - f = open(self.dropbox_path, 'rb') - try: - client.files_upload(f.read(), dest_file) - except DropboxApiError as err: - utils.log(u'Dropbox upload error: ' + str(err)) - utils.showNotification(utils.getString(32708), utils.getString(32709), xbmc.LOGERROR) - return - finally: - f.close() - utils.showNotification(utils.getString(32713), utils.getString(32714), xbmc.LOGINFO) - utils.log(u'Dropbox upload complete: %s -> %s' % (self.dropbox_path, dest_file)) - - def pullFromDropbox(self): - """ - Downloads the dropbox database - - Returns: - return code: - 0 successfully downloaded databases - 1 database download error - """ - dropbox_key = utils.getSetting('dropbox_apikey') - - if not dropbox_key: - # no dropbox authorization key entered. Feature does not work. - utils.showNotification(utils.getString(32708), utils.getString(32715), xbmc.LOGWARNING) - return 0 - - utils.log(u'Downloading WatchedList from dropbox') - - client = dropbox.Dropbox(dropbox_key) - - # save the dropbox database file in the user data directory (this is only a temporary file for upload and download) - self.dropbox_path = os.path.join(utils.data_dir(), "dropbox.db") - dropbox_file_exists = False - - remote_file = '/' + 'watchedlist.db' - old_file = '/old' + 'watchedlist.db' - # first: Try downloading the database file (if existent). - # if that fails, try restoring the backup file (if existent). - # if that also fails, the database will be rewritten (in other function) - try: - trycount = 0 - while trycount <= 4: # two tries for db file (trycount 1, 2), then try copying backup and download that (trycount 3,4) - if self.monitor.abortRequested(): - break - trycount = trycount + 1 - try: - client.files_download_to_file(self.dropbox_path, remote_file) - dropbox_file_exists = True - break - except DropboxApiError as err: - # file not available, e.g. deleted or first execution. - utils.log(u'Dropbox database download failed. %s.' % str(err)) - time.sleep(0.5) # wait to avoid immediate re-try - if err[1].is_path() and isinstance(err[1].get_path(), dropbox.files.LookupError): - # Error reason: file does not exist - trycount = trycount + 1 # do not try second time on downloading non-existing file - if trycount == 2 and not dropbox_file_exists: - try: - # No file was downloaded after second try. That means the dropbox file does not exist - # Try restoring the backup file, if existent - client.files_copy(old_file, remote_file) - except DropboxApiError as err: # files_copy raises RelocationError - # file not available, e.g. deleted or not existing - utils.log(u'Dropbox restore database backup failed. %s.' % str(err)) - break - except BaseException: # catch this error, the dropbox mode will be disabled - utils.log(u'Dropbox download error: ' + str(sys.exc_info())) - utils.showNotification(utils.getString(32708), utils.getString(32710), xbmc.LOGERROR) - self.dropbox_path = '' - return 1 - # check if file was written - if dropbox_file_exists: - utils.log(u'Dropbox database downloaded: %s -> %s' % (remote_file, self.dropbox_path)) - else: - utils.log(u'Dropbox database download failed. No remote file available.') - return 0 - - def get_movie_status(self, imdbId): - """ - Return the watched state of one movie from the internal WL database copy - requires this class to have a loaded database - - Args: - imdbId (Integer) - - Returns: - List with [Playcount, lastPlayed (integer timestamp), lastChange (integer timestamp)] - if no entry is available, playcount is -1 - """ - j = [ii for ii, x in enumerate(self.watchedmovielist_wl) if x[0] == imdbId] - if not j: - return [-1, 0, 0] - return [self.watchedmovielist_wl[j[0]][x] for x in [4, 3, 6]] # return 4playCount, 3lastPlayed, 6lastChange - - def get_episode_status(self, tvdbId, season, episode): - """ - Return the watched state of one tv series episode from the internal WL database copy - requires this class to have a loaded database - - Args: - tvdbId, season, episode (Integer) - - Returns: - List with [Playcount, lastPlayed (integer timestamp), lastChange (integer timestamp)] - if no entry is available, playcount is -1 - """ - j = [ii for ii, x in enumerate(self.watchedepisodelist_wl) if x[0] == tvdbId and x[1] == season and x[2] == episode] - if not j: - return [-1, 0, 0] - return [self.watchedepisodelist_wl[j[0]][x] for x in [4, 3, 6]] # return 4playCount, 3lastPlayed, 6lastChange diff --git a/service.watchedlist/manual.py b/service.watchedlist/manual.py deleted file mode 100644 index 1a18e3a64..000000000 --- a/service.watchedlist/manual.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -This file is entry point for manual start via the programs menu -""" -import sys - -import xbmc -import xbmcgui - -import lib.watchedlist.utils as utils -from lib.watchedlist.watchedlist import WatchedList - - -__remotedebug__ = False -# append pydev remote debugger -if __remotedebug__: - utils.log("Initialize remote debugging.") - # Make pydev debugger works for auto reload. - try: - import pydevd - pydevd.settrace('localhost', port=60678, stdoutToServer=True, stderrToServer=True) - except ImportError: - sys.stderr.write("Error: " + - "You must add org.python.pydev.debug.pysrc to your PYTHONPATH.") - utils.showNotification('WatchedList Error', 'remote debug could not be imported.', xbmc.LOGFATAL) - sys.exit(1) - except BaseException: - utils.showNotification('WatchedList Error', 'remote debug in pydev is activated, but remote server not responding.', xbmc.LOGERROR) - sys.exit(1) - -if (not utils.getSetting("autostart") == 'true') or xbmcgui.Dialog().yesno(utils.getString(32101), utils.getString(32001)): - # Check if we should run updates (only ask if autostart is on) - # run the program - utils.log("Update Library Manual Run.") - WatchedList().runUpdate(True) # one time update diff --git a/service.watchedlist/resources/language/resource.language.de_de/strings.po b/service.watchedlist/resources/language/resource.language.de_de/strings.po deleted file mode 100644 index 3eeb7c43b..000000000 --- a/service.watchedlist/resources/language/resource.language.de_de/strings.po +++ /dev/null @@ -1,389 +0,0 @@ -# Kodi Media Center language file -# Addon Name: WatchedList -# Addon id: service.watchedlist -# Addon Provider: schapplm -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2016-03-28 16:47+0000\n" -"PO-Revision-Date: 2017-12-30 10:56+0100\n" -"Last-Translator: schapplm\n" -"Language-Team: schapplm\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: de_DE\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "Addon Summary" -msgid "Save watched Movies and TV Episodes independent from filenames" -msgstr "Gesehen-Status von Filmen und Serien-Episoden unabhängig von Dateinamen speichern" - -msgctxt "Addon Description" -msgid "Export: Searches the Kodi-Database for watched files. Determine imdb-id and thetvdb-id to identify movies and TV-episodes. Then save the list to a new independent table.\nImport: Set the watched state for each video file in Kodi.\nAutomatic background process without user interaction." -msgstr "Export: Durchsucht die Kodi-Datenbank nach gesehenen Filmen und ordnet den Dateien die eindeutigen Kennnummern von imdb und TheTVDB zu. Gesehene Filme und Episoden werden jeweils in einer eigenen Datenbank gespeichert.\nImport: Der Gesehen-Status von Medien in Kodi wird aus der Gesehenen-Tabelle geholt.\nDie Synchronisierung geschieht automatisch im Hintergrund ohne Benutzereingriff." - -msgctxt "Addon Disclaimer" -msgid "This script does not regard the file format. A Movie watched in DVD quality will also set the 1080p version as watched." -msgstr "Dieses Script ignoriert das Dateiformat. Ein gesehener Film in DVD-Qualität wird auch in der BlueRay-Version als gesehen gekennzeichnet." - -msgctxt "#30001" -msgid "Basic Settings" -msgstr "Grundeinstellungen" - -msgctxt "#30002" -msgid "Database" -msgstr "Datenbank" - -msgctxt "#30011" -msgid "Update movies watched state" -msgstr "Aktualisiere Gesehen-Status von Filmen" - -msgctxt "#30012" -msgid "Update episodes watched state" -msgstr "Aktualisiere Gesehen-Status von Episoden" - -msgctxt "#30013" -msgid "autostart mode" -msgstr "Starttyp" - -msgctxt "#30014" -msgid "Update interval [hours]" -msgstr "Aktualisierungs-Intervall [Stunden]" - -msgctxt "#30015" -msgid "Startup delay before action [minutes]" -msgstr "Start-Verzögerung [Minuten]" - -msgctxt "#30016" -msgid "Progress Dialog" -msgstr "Fortschrittsanzeige" - -msgctxt "#30017" -msgid "Use non-default database file" -msgstr "Eigene Datenbank-Datei" - -msgctxt "#30018" -msgid "Path to database file" -msgstr "Pfad zur Datenbank-Datei" - -msgctxt "#30019" -msgid "Database filename (SQLite Database, *.db)" -msgstr "Dateiname (*.db)" - -msgctxt "#30020" -msgid "Number of backup copies (-1=infinite)" -msgstr "Anzahl an Sicherungskopien (-1=unendlich)" - -msgctxt "#30021" -msgid "Start with Kodi (autorun)" -msgstr "Automatisch mit Kodi starten" - -msgctxt "#30022" -msgid "follow user setting watched/unwatched" -msgstr "Gesehen-Änderungen laufend überwachen" - -msgctxt "#30023" -msgid "Dropbox Auth Code" -msgstr "Dropbox Autorisierungs-Code" - -msgctxt "#30024" -msgid "Amount of User Information" -msgstr "Grad der Benutzerinformation" - -msgctxt "#30025" -msgid "All" -msgstr "Alle" - -msgctxt "#30026" -msgid "Only Info" -msgstr "Nur Infos" - -msgctxt "#30027" -msgid "Only Warning" -msgstr "Nur Warnungen" - -msgctxt "#30028" -msgid "Only Error" -msgstr "Nur Fehler" - -msgctxt "#30029" -msgid "None" -msgstr "Keine" - -msgctxt "#30030" -msgid "no update" -msgstr "Keine Aktualisierung" - -msgctxt "#30031" -msgid "one update" -msgstr "Einmalig" - -msgctxt "#30032" -msgid "periodic" -msgstr "Periodisch" - -msgctxt "#30040" -msgid "DB Method" -msgstr "DB-Art" - -msgctxt "#30041" -msgid "File" -msgstr "Datei" - -msgctxt "#30042" -msgid "MySQL" -msgstr "MySQL" - -msgctxt "#30043" -msgid "Options for accessing SQLite-DB-File" -msgstr "Optionen für SQLite-Datei-Zugriff" - -msgctxt "#30044" -msgid "Options for accessing MySQL-Server" -msgstr "Optionen für MySQL-Server-Zugriff" - -msgctxt "#30045" -msgid "Dropbox synchronization" -msgstr "Dropbox-Synchronisierung" - -msgctxt "#30051" -msgid "MySQL Server" -msgstr "MySQL Server" - -msgctxt "#30052" -msgid "MySQL Database" -msgstr "MySQL Datenbank" - -msgctxt "#30053" -msgid "MySQL Username" -msgstr "MySQL Benutzername" - -msgctxt "#30054" -msgid "MySQL Password" -msgstr "MySQL Kennwort" - -msgctxt "#30055" -msgid "MySQL Port" -msgstr "MySQL Port" - -msgctxt "#32001" -msgid "manually run the database update again?" -msgstr "Datenbank-Aktualisierung nochmal starten?" - -msgctxt "#32002" -msgid "DB path does not exist: %s" -msgstr "DB-Pfad existiert nicht: %s" - -msgctxt "#32003" -msgid "next update in %d hours" -msgstr "Nächste Aktualisierung in %d Stunden" - -msgctxt "#32004" -msgid "begin update in %d minutes" -msgstr "Starte Aktualisierung in %d Minuten" - -msgctxt "#32005" -msgid "running observation of changes of watched states" -msgstr "laufende Überwachung von Änderungen des Gesehen-Status" - -msgctxt "#32101" -msgid "WatchedList" -msgstr "WatchedList" - -msgctxt "#32102" -msgid "Watchedlist: Error" -msgstr "WatchedList: Fehler" - -msgctxt "#32103" -msgid "Load watched states from addon-database..." -msgstr "Lade Gesehen-Status aus Gesehen-Datenbank..." - -msgctxt "#32104" -msgid "load watched states Kodi-database ..." -msgstr "Lade Gesehene-Status aus Kodi-Datenbank ..." - -msgctxt "#32105" -msgid "update the addon watched-database ..." -msgstr "Aktualisiere die Gesehen-Datenbank: ..." - -msgctxt "#32106" -msgid "update the Kodi-database ..." -msgstr "Aktualisiere die Kodi-Datenbank ..." - -msgctxt "#32107" -msgid "update complete." -msgstr "Aktualisierung abgeschlossen." - -msgctxt "#32108" -msgid "Watchedlist: MySQL Error (WL DB)" -msgstr "Watchedlist: MySQL Fehler (WL DB)" - -msgctxt "#32109" -msgid "Watchedlist: SQLite Error (WL DB)" -msgstr "Watchedlist: SQLite Fehler (WL DB)" - -msgctxt "#32202" -msgid "Addon movie database" -msgstr "Gesehen-Datenbank (Filme)" - -msgctxt "#32203" -msgid "Addon episode database" -msgstr "Gesehen-Datenbank (Episoden)" - -msgctxt "#32204" -msgid "Kodi-movie database" -msgstr "Kodi-Filmdatenbank" - -msgctxt "#32205" -msgid "Kodi-episode database" -msgstr "Kodi-Episodendatenbank" - -msgctxt "#32208" -msgid "Access Denied (username/password/rights)" -msgstr "Zugriff verweigert (Benutzername/Kennwort/Rechte)" - -msgctxt "#32209" -msgid "Database '%s' does not exist" -msgstr "Datenbank '%s' existiert nicht" - -msgctxt "#32210" -msgid "Access denied (user '%s', DB '%s')" -msgstr "Zugriff verweigert (Benutzer '%s', DB '%s')" - -msgctxt "#32297" -msgid "TV-Show \"%s\" (ID %d) has no tvdb-number. No processing with WL possible. Try rescraping." -msgstr "TV-Serie \"%s\" (ID %d) hat keine tvdb-Nummer. Keine Verarbeitung mit WL möglich." - -msgctxt "#32298" -msgid "%d movies and %d episodes read from WL." -msgstr "%d Filme, %d Episoden in WL." - -msgctxt "#32299" -msgid "%d movies and %d episodes read from Kodi-DB." -msgstr "%d Filme, %d Episoden in Kodi-DB." - -msgctxt "#32301" -msgid "%d watched new. %d updated" -msgstr "%d neu gesehen. %d aktualisiert." - -msgctxt "#32302" -msgid "%d movies marked as watched" -msgstr "%d Filme als gesehen markiert." - -msgctxt "#32303" -msgid "%d episodes marked as watched" -msgstr "%d Episoden als gesehen markiert." - -msgctxt "#32401" -msgid "Set as watched (Kodi-DB)" -msgstr "Als gesehen markiert (Kodi-DB)" - -msgctxt "#32402" -msgid "Remember as newly watched (WL-DB)" -msgstr "Merke als neu gesehen (WL-DB)" - -msgctxt "#32403" -msgid "Remember as watched (updated) (WL-DB)" -msgstr "Merke als gesehen (aktualisiert) (WL-DB)" - -msgctxt "#32404" -msgid "Set as not watched (Kodi-DB)" -msgstr "Als ungesehen markiert (Kodi-DB)" - -msgctxt "#32405" -msgid "Remember as not watched (WL-DB)" -msgstr "Merke als ungesehen (WL-DB)" - -msgctxt "#32601" -msgid "Error opening WL-database" -msgstr "Öffnen der Gesehen-Datenbank" - -msgctxt "#32602" -msgid "Error loading WL-database" -msgstr "Laden der Gesehen-Datenbank" - -msgctxt "#32603" -msgid "Error loading Kodi-database" -msgstr "Laden der Kodi-Datenbank" - -msgctxt "#32604" -msgid "Error updating WL database" -msgstr "Aktualisieren der Gesehen-Datenbank" - -msgctxt "#32605" -msgid "Error updating Kodi-database" -msgstr "Aktualisieren der Kodi-Datenbank" - -msgctxt "#32606" -msgid "Writing the WL-database file %s" -msgstr "Beim Schreiben der Gesehen-Datenbank %s" - -msgctxt "#32607" -msgid "Unable to merge dropbox data" -msgstr "Zusammenführung mit Dropbox" - -msgctxt "#32608" -msgid "Doing a database backup (%s)" -msgstr "Beim Sichern der Datenbank (%s)" - -msgctxt "#32610" -msgid "%d/%d: %s" -msgstr "%d/%d: %s" - -msgctxt "#32611" -msgid "%d/%d: %s S%02dE%02d" -msgstr "%d/%d: %s S%02dE%02d" - -msgctxt "#32708" -msgid "Dropbox Error" -msgstr "Dropbox Fehler" - -msgctxt "#32709" -msgid "Unable to upload" -msgstr "Hochladen nicht möglich" - -msgctxt "#32710" -msgid "Unable to download" -msgstr "Herunterladen nicht möglich" - -msgctxt "#32711" -msgid "WatchedList: Dropbox-Import (Movies)" -msgstr "WatchedList: Dropbox-Import (Filme)" - -msgctxt "#32712" -msgid "WatchedList: Dropbox-Import (Episodes)" -msgstr "WatchedList: Dropbox-Import (Episoden)" - -msgctxt "#32713" -msgid "Dropbox synchronization" -msgstr "Dropbox-Synchronisierung" - -msgctxt "#32714" -msgid "Upload successfull" -msgstr "Hochladen erfolgreich" - -msgctxt "#32715" -msgid "Empty authorization code in settings" -msgstr "Leerer Autorisierungscode in Einstellungen" - -msgctxt "#32716" -msgid "Update the Dropbox database: ..." -msgstr "Aktualisiere die Dropbox-Datenbank: ..." - -msgctxt "#32717" -msgid "Dropbox database updated: %d entries." -msgstr "Dropbox-Datenbank aktualisiert. %d Einträge." - -msgctxt "#32718" -msgid "WatchedList: Dropbox-Export (Movies)" -msgstr "WatchedList: Dropbox-Export (Filme)" - -msgctxt "#32719" -msgid "WatchedList: Dropbox-Export (Episodes)" -msgstr "WatchedList: Dropbox-Export (Episoden)" - -msgctxt "#32720" -msgid "Error importing python Dropbox API. Disable function." -msgstr "Fehler beim Laden der Dropbox API. Deaktiviere Funktion." diff --git a/service.watchedlist/resources/language/resource.language.en_gb/strings.po b/service.watchedlist/resources/language/resource.language.en_gb/strings.po deleted file mode 100644 index e64d9df32..000000000 --- a/service.watchedlist/resources/language/resource.language.en_gb/strings.po +++ /dev/null @@ -1,419 +0,0 @@ -# Kodi Media Center language file -# Addon Name: WatchedList -# Addon id: service.watchedlist -# Addon Provider: schapplm -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2016-03-28 16:47+0000\n" -"PO-Revision-Date: 2017-12-30 10:56+0100\n" -"Last-Translator: schapplm\n" -"Language-Team: schapplm\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en_GB\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "Addon Summary" -msgid "Save watched Movies and TV Episodes independent from filenames" -msgstr "" - -msgctxt "Addon Description" -msgid "Export: Searches the Kodi-Database for watched files. Determine imdb-id and thetvdb-id to identify movies and TV-episodes. Then save the list to a new independent table.\nImport: Set the watched state for each video file in Kodi.\nAutomatic background process without user interaction." -msgstr "" - -msgctxt "Addon Disclaimer" -msgid "This script does not regard the file format. A Movie watched in DVD quality will also set the 1080p version as watched." -msgstr "" - -# Notification headers - -msgctxt "#30001" -msgid "Basic Settings" -msgstr "" - -msgctxt "#30002" -msgid "Database" -msgstr "" - -msgctxt "#30011" -msgid "Update movies watched state" -msgstr "" - -msgctxt "#30012" -msgid "Update episodes watched state" -msgstr "" - -msgctxt "#30013" -msgid "autostart mode" -msgstr "" - -msgctxt "#30014" -msgid "Update interval [hours]" -msgstr "" - -msgctxt "#30015" -msgid "Startup delay before action [minutes]" -msgstr "" - -msgctxt "#30016" -msgid "Progress Dialog" -msgstr "" - -msgctxt "#30017" -msgid "Use non-default database file" -msgstr "" - -msgctxt "#30018" -msgid "Path to database file" -msgstr "" - -msgctxt "#30019" -msgid "Database filename (SQLite Database, *.db)" -msgstr "" - -msgctxt "#30020" -msgid "Number of backup copies (-1=infinite)" -msgstr "" - -msgctxt "#30021" -msgid "Start with Kodi (autorun)" -msgstr "" - -msgctxt "#30022" -msgid "follow user setting watched/unwatched" -msgstr "" - -msgctxt "#30023" -msgid "Dropbox Auth Code" -msgstr "" - -msgctxt "#30024" -msgid "Amount of User Information" -msgstr "" - -msgctxt "#30025" -msgid "All" -msgstr "" - -msgctxt "#30026" -msgid "Only Info" -msgstr "" - -msgctxt "#30027" -msgid "Only Warning" -msgstr "" - -msgctxt "#30028" -msgid "Only Error" -msgstr "" - -msgctxt "#30029" -msgid "None" -msgstr "" - -msgctxt "#30030" -msgid "no update" -msgstr "" - -msgctxt "#30031" -msgid "one update" -msgstr "" - -msgctxt "#30032" -msgid "periodic" -msgstr "" - -# empty strings from id 30033 to 30039 - -msgctxt "#30040" -msgid "DB Method" -msgstr "" - -msgctxt "#30041" -msgid "File" -msgstr "" - -msgctxt "#30042" -msgid "MySQL" -msgstr "" - -msgctxt "#30043" -msgid "Options for accessing SQLite-DB-File" -msgstr "" - -msgctxt "#30044" -msgid "Options for accessing MySQL-Server" -msgstr "" - -msgctxt "#30045" -msgid "Dropbox synchronization" -msgstr "" - -# empty strings from id 30046 to 30050 - -msgctxt "#30051" -msgid "MySQL Server" -msgstr "" - -msgctxt "#30052" -msgid "MySQL Database" -msgstr "" - -msgctxt "#30053" -msgid "MySQL Username" -msgstr "" - -msgctxt "#30054" -msgid "MySQL Password" -msgstr "" - -msgctxt "#30055" -msgid "MySQL Port" -msgstr "" - -# empty strings from id 30056 to 32000 - -msgctxt "#32001" -msgid "manually run the database update again?" -msgstr "" - -msgctxt "#32002" -msgid "DB path does not exist: %s" -msgstr "" - -msgctxt "#32003" -msgid "next update in %d hours" -msgstr "" - -msgctxt "#32004" -msgid "begin update in %d minutes" -msgstr "" - -msgctxt "#32005" -msgid "running observation of changes of watched states" -msgstr "" - -# empty strings from id 32006 to 32100 - -msgctxt "#32101" -msgid "WatchedList" -msgstr "" - -msgctxt "#32102" -msgid "Watchedlist: Error" -msgstr "" - -msgctxt "#32103" -msgid "Load watched states from addon-database..." -msgstr "" - -msgctxt "#32104" -msgid "load watched states Kodi-database ..." -msgstr "" - -msgctxt "#32105" -msgid "update the addon watched-database ..." -msgstr "" - -msgctxt "#32106" -msgid "update the Kodi-database ..." -msgstr "" - -msgctxt "#32107" -msgid "update complete." -msgstr "" - -msgctxt "#32108" -msgid "Watchedlist: MySQL Error (WL DB)" -msgstr "" - -msgctxt "#32109" -msgid "Watchedlist: SQLite Error (WL DB)" -msgstr "" - -# empty strings from id 32110 to 32201 - -msgctxt "#32202" -msgid "Addon movie database" -msgstr "" - -msgctxt "#32203" -msgid "Addon episode database" -msgstr "" - -msgctxt "#32204" -msgid "Kodi-movie database" -msgstr "" - -msgctxt "#32205" -msgid "Kodi-episode database" -msgstr "" - -# empty strings from id 32206 to 32207 - -msgctxt "#32208" -msgid "Access Denied (username/password/rights)" -msgstr "" - -msgctxt "#32209" -msgid "Database '%s' does not exist" -msgstr "" - -msgctxt "#32210" -msgid "Access denied (user '%s', DB '%s')" -msgstr "" - -# empty strings from id 32211 to 32296 - -msgctxt "#32297" -msgid "TV-Show \"%s\" (ID %d) has no tvdb-number. No processing with WL possible. Try rescraping." -msgstr "" - -msgctxt "#32298" -msgid "%d movies and %d episodes read from WL." -msgstr "" - -msgctxt "#32299" -msgid "%d movies and %d episodes read from Kodi-DB." -msgstr "" - -# empty string with id 32300 - -msgctxt "#32301" -msgid "%d watched new. %d updated" -msgstr "" - -msgctxt "#32302" -msgid "%d movies marked as watched" -msgstr "" - -msgctxt "#32303" -msgid "%d episodes marked as watched" -msgstr "" - -# Error Messages -# empty strings from id 32304 to 32400 - -msgctxt "#32401" -msgid "Set as watched (Kodi-DB)" -msgstr "" - -msgctxt "#32402" -msgid "Remember as newly watched (WL-DB)" -msgstr "" - -msgctxt "#32403" -msgid "Remember as watched (updated) (WL-DB)" -msgstr "" - -msgctxt "#32404" -msgid "Set as not watched (Kodi-DB)" -msgstr "" - -msgctxt "#32405" -msgid "Remember as not watched (WL-DB)" -msgstr "" - -# Notification text -# empty strings from id 32406 to 32600 - -msgctxt "#32601" -msgid "Error opening WL-database" -msgstr "" - -msgctxt "#32602" -msgid "Error loading WL-database" -msgstr "" - -msgctxt "#32603" -msgid "Error loading Kodi-database" -msgstr "" - -msgctxt "#32604" -msgid "Error updating WL database" -msgstr "" - -msgctxt "#32605" -msgid "Error updating Kodi-database" -msgstr "" - -msgctxt "#32606" -msgid "Writing the WL-database file %s" -msgstr "" - -msgctxt "#32607" -msgid "Unable to merge dropbox data" -msgstr "" - -msgctxt "#32608" -msgid "Doing a database backup (%s)" -msgstr "" - -# Progress Dialog -# empty string with id 32609 - -msgctxt "#32610" -msgid "%d/%d: %s" -msgstr "" - -msgctxt "#32611" -msgid "%d/%d: %s S%02dE%02d" -msgstr "" - -# Dropbox related messages -# empty strings from id 32612 to 32707 - -msgctxt "#32708" -msgid "Dropbox Error" -msgstr "" - -msgctxt "#32709" -msgid "Unable to upload" -msgstr "" - -msgctxt "#32710" -msgid "Unable to download" -msgstr "" - -msgctxt "#32711" -msgid "WatchedList: Dropbox-Import (Movies)" -msgstr "" - -msgctxt "#32712" -msgid "WatchedList: Dropbox-Import (Episodes)" -msgstr "" - -msgctxt "#32713" -msgid "Dropbox synchronization" -msgstr "" - -msgctxt "#32714" -msgid "Upload successfull" -msgstr "" - -msgctxt "#32715" -msgid "Empty authorization code in settings" -msgstr "" - -msgctxt "#32716" -msgid "Update the Dropbox database: ..." -msgstr "" - -msgctxt "#32717" -msgid "Dropbox database updated: %d entries." -msgstr "" - -msgctxt "#32718" -msgid "WatchedList: Dropbox-Export (Movies)" -msgstr "" - -msgctxt "#32719" -msgid "WatchedList: Dropbox-Export (Episodes)" -msgstr "" - -msgctxt "#32720" -msgid "Error importing python Dropbox API. Disable function." -msgstr "" diff --git a/service.watchedlist/resources/language/resource.language.he_il/strings.po b/service.watchedlist/resources/language/resource.language.he_il/strings.po deleted file mode 100644 index 610f7762d..000000000 --- a/service.watchedlist/resources/language/resource.language.he_il/strings.po +++ /dev/null @@ -1,409 +0,0 @@ -# Kodi Media Center language file -# Addon Name: WatchedList -# Addon id: service.watchedlist -# Addon Provider: schapplm -msgid "" -msgstr "" -"Project-Id-Version: XBMC-Addons\n" -"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2016-03-28 16:47+0000\n" -"PO-Revision-Date: 2017-10-09 11:16+0300\n" -"Last-Translator: A. Dambledore\n" -"Language-Team: Eng2Heb\n" -"Language: he_IL\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - - -msgctxt "Addon Summary" -msgid "Save watched Movies and TV Episodes independent from filenames" -msgstr "שומר מצב נצפה לסרטים ופרקים טלוויזיה עצמאית משמות קבצים" - -msgctxt "Addon Description" -msgid "" -"Export: Searches the Kodi-Database for watched files. Determine imdb-id and thetvdb-id to identify movies and TV-episodes. Then save the list to a new independent table.\n" -"Import: Set the watched state for each video file in Kodi.\n" -"Automatic background process without user interaction." -msgstr "" -"ייצוא: מחפש את מסד הנתונים של קודי עבור קבצים שנצפו. קבע את iddb-id ו- thetvdb-id כדי לזהות סרטים ופרטי טלוויזיה. לאחר מכן שמור את הרשימה בטבלה עצמאית חדשה.\n" -"ייבוא: הגדר את מצב נצפה עבור כל קובץ וידאו בקודי.\n" -"תהליך רקע אוטומטי ללא אינטראקציה עם המשתמש." - -msgctxt "Addon Disclaimer" -msgid "This script does not regard the file format. A Movie watched in DVD quality will also set\tthe 1080p version as watched." -msgstr "הסקריפט הזה אינו מתייחס לתבנית הקובץ. סרט שנצפה באיכות DVD, יקבע גם את גרסת 1080p כנצפה." - -# Notification headers -msgctxt "#30001" -msgid "Basic Settings" -msgstr "הגדרות בסיסיות" - -msgctxt "#30002" -msgid "Database" -msgstr "מסד נתונים" - -msgctxt "#30011" -msgid "Update movies watched state" -msgstr "עדכון מצב נצפה לסרטים" - -msgctxt "#30012" -msgid "Update episodes watched state" -msgstr "עדכון מצב נצפה לפרקים" - -msgctxt "#30013" -msgid "autostart mode" -msgstr "מצב אתחול אוטומטי" - -msgctxt "#30014" -msgid "Update interval [hours]" -msgstr "מרווח בין עדכונים (שעות)" - -msgctxt "#30015" -msgid "Startup delay before action [minutes]" -msgstr "השהיית הפעלה לפני פעולה [דקות]" - -msgctxt "#30016" -msgid "Progress Dialog" -msgstr "דיאלוג התקדמות" - -msgctxt "#30017" -msgid "Use non-default database file" -msgstr "השתמש בקובץ מסד נתונים שאינו ברירת מחדל" - -msgctxt "#30018" -msgid "Path to database file" -msgstr "הנתיב אל קובץ מסד הנתונים" - -msgctxt "#30019" -msgid "Database filename (SQLite Database, *.db)" -msgstr "שם הקובץ של מסד הנתונים (מסד הנתונים SQLite, * .db)" - -msgctxt "#30020" -msgid "Number of backup copies (-1=infinite)" -msgstr "מספר עותקי גיבוי (-1 = אינסופי)" - -msgctxt "#30021" -msgid "Start with Kodi (autorun)" -msgstr "להתחיל עם קודי (הפעלה אוטומטית)" - -msgctxt "#30022" -msgid "follow user setting watched/unwatched" -msgstr "עקוב אחר הגדרות המשתמש למצב נצפה או לא נצפה" - -msgctxt "#30023" -msgid "Dropbox Auth Code" -msgstr "קוד אימות בדרופבוקס" - -msgctxt "#30024" -msgid "Amount of User Information" -msgstr "כמות מידע משתמש" - -msgctxt "#30025" -msgid "All" -msgstr "הכול" - -msgctxt "#30026" -msgid "Only Info" -msgstr "רק מידע" - -msgctxt "#30027" -msgid "Only Warning" -msgstr "רק אזהרה" - -msgctxt "#30028" -msgid "Only Error" -msgstr "רק שגיאה" - -msgctxt "#30029" -msgid "None" -msgstr "ללא" - -msgctxt "#30030" -msgid "no update" -msgstr "אין עדכון" - -msgctxt "#30031" -msgid "one update" -msgstr "עדכון אחד" - -msgctxt "#30032" -msgid "periodic" -msgstr "תקופתיים" - -# empty strings from id 30033 to 30039 -msgctxt "#30040" -msgid "DB Method" -msgstr "שיטת DB" - -msgctxt "#30041" -msgid "File" -msgstr "קובץ" - -msgctxt "#30042" -msgid "MySQL" -msgstr "MySQL" - -msgctxt "#30043" -msgid "Options for accessing SQLite-DB-File" -msgstr "אפשרויות לגישה SQLite-DB-File" - -msgctxt "#30044" -msgid "Options for accessing MySQL-Server" -msgstr "אפשרויות לגישה שרת MySQL" - -msgctxt "#30045" -msgid "Dropbox synchronization" -msgstr "סנכרון דרופבוקס" - -# empty strings from id 30046 to 30050 -msgctxt "#30051" -msgid "MySQL Server" -msgstr "MySQL שרת" - -msgctxt "#30052" -msgid "MySQL Database" -msgstr "MySQL מסד הנתונים" - -msgctxt "#30053" -msgid "MySQL Username" -msgstr "MySQL שם המשתמש" - -msgctxt "#30054" -msgid "MySQL Password" -msgstr "MySQL סיסמה" - -msgctxt "#30055" -msgid "MySQL Port" -msgstr "MySQL פורט" - -# empty strings from id 30056 to 32000 -msgctxt "#32001" -msgid "manually run the database update again?" -msgstr "להפעיל באופן ידני את העדכון מסד הנתונים שוב?" - -msgctxt "#32002" -msgid "DB path does not exist: %s" -msgstr "לא קיים נתיב לבסיס הנתונים: %s" - -msgctxt "#32003" -msgid "next update in %d hours" -msgstr "העדכון הבא בעוד %d שעות" - -msgctxt "#32004" -msgid "begin update in %d minutes" -msgstr "העדכון הבא בעוד %d דקות" - -msgctxt "#32005" -msgid "running observation of changes of watched states" -msgstr "מריץ סריקה לשינויים במצב נצפה" - -# empty strings from id 32006 to 32100 -msgctxt "#32101" -msgid "WatchedList" -msgstr "רשימת נצפה" - -msgctxt "#32102" -msgid "Watchedlist: Error" -msgstr "רשימת נצפה: שגיאה" - -msgctxt "#32103" -msgid "Load watched states from addon-database..." -msgstr "טוען מצב נצפה מתוך מסד הנתונים של ההרחבה..." - -msgctxt "#32104" -msgid "load watched states Kodi-database ..." -msgstr "טוען מצב נצפה מתוך מסד הנתונים של קודי..." - -msgctxt "#32105" -msgid "update the addon watched-database ..." -msgstr "עדכון את מצב נצפה במסד הנתונים של ההרחבה..." - -msgctxt "#32106" -msgid "update the Kodi-database ..." -msgstr "עדכון את מסד הנתונים של קודי..." - -msgctxt "#32107" -msgid "update complete." -msgstr "העדכון הושלם..." - -msgctxt "#32108" -msgid "Watchedlist: MySQL Error (WL DB)" -msgstr "רשימת נצפה: שגיאה MySQL (WL DB)" - -msgctxt "#32109" -msgid "Watchedlist: SQLite Error (WL DB)" -msgstr "רשימת נצפה: שגיאה SQLite (WL DB)" - -# empty strings from id 32110 to 32201 -msgctxt "#32202" -msgid "Addon movie database" -msgstr "מסד נתוני סרטים של ההרחבה" - -msgctxt "#32203" -msgid "Addon episode database" -msgstr "מסד נתוני פרקים של ההרחבה" - -msgctxt "#32204" -msgid "Kodi-movie database" -msgstr "מסד נתוני סרטים של קודי" - -msgctxt "#32205" -msgid "Kodi-episode database" -msgstr "מסד נתוני פרקים של קודי" - -# empty strings from id 32206 to 32207 -msgctxt "#32208" -msgid "Access Denied (username/password/rights)" -msgstr "הגישה נדחתה (שם משתמש/סיסמה/זכויות)" - -msgctxt "#32209" -msgid "Database '%s' does not exist" -msgstr "מסד הנתונים '%s' אינו קיים" - -msgctxt "#32210" -msgid "Access denied (user '%s', DB '%s')" -msgstr "הגישה נדחתה (המשתמש '%s', DB '%s')" - -# empty strings from id 32211 to 32296 -msgctxt "#32297" -msgid "TV-Show \"%s\" (ID %d) has no tvdb-number. No processing with WL possible. Try rescraping." -msgstr "סדרות \"%s\" (מזהה %d) אין מספר-tvdb. אין אפשרות לעבד עם WL. נסה לבצע סקרייפינג מחדש." - -msgctxt "#32298" -msgid "%d movies and %d episodes read from WL." -msgstr "%d סרטים, פרקים %d נקראו מ-WL." - -msgctxt "#32299" -msgid "%d movies and %d episodes read from Kodi-DB." -msgstr "%d סרטים, פרקים %d נקראו ממסד הנתונים של קודי." - -# empty string with id 32300 -msgctxt "#32301" -msgid "%d watched new. %d updated" -msgstr "%d מצב נצפה חדש. %d עודכן" - -msgctxt "#32302" -msgid "%d movies marked as watched" -msgstr "%d סרטים שסומנו כנצפה" - -msgctxt "#32303" -msgid "%d episodes marked as watched" -msgstr "%d פרקים שסומנו כנצפה" - -# Error Messages -# empty strings from id 32304 to 32400 -msgctxt "#32401" -msgid "Set as watched (Kodi-DB)" -msgstr "הגדר כנצפה (קודי-DB)" - -msgctxt "#32402" -msgid "Remember as newly watched (WL-DB)" -msgstr "זכור כנצפה לאחרונה (WL-DB)" - -msgctxt "#32403" -msgid "Remember as watched (updated) (WL-DB)" -msgstr "זכור כנצפה (מעודכן) (WL-DB)" - -msgctxt "#32404" -msgid "Set as not watched (Kodi-DB)" -msgstr "הגדר כלא נצפה (קודי-DB)" - -msgctxt "#32405" -msgid "Remember as not watched (WL-DB)" -msgstr "זכור כלא נצפה (WL-DB)" - -# Notification text -# empty strings from id 32406 to 32600 -msgctxt "#32601" -msgid "Error opening WL-database" -msgstr "שגיאה בפתיחת מסד הנתונים WL" - -msgctxt "#32602" -msgid "Error loading WL-database" -msgstr "שגיאה בטעינת מסד הנתונים WL" - -msgctxt "#32603" -msgid "Error loading Kodi-database" -msgstr "שגיאה בטעינת מסד הנתונים של קודי" - -msgctxt "#32604" -msgid "Error updating WL database" -msgstr "שגיאה בעת עדכון מסד נתונים WL" - -msgctxt "#32605" -msgid "Error updating Kodi-database" -msgstr "שגיאה בעת עדכון מסד נתונים קודי" - -msgctxt "#32606" -msgid "Writing the WL-database file %s" -msgstr "כתיבת קובץ WL-מסד הנתונים %s" - -msgctxt "#32607" -msgid "Unable to merge dropbox data" -msgstr "אין אפשרות למזג נתונים מדרופבוקס" - -# Progress Dialog -# empty string with id 32609 -msgctxt "#32610" -msgid "%d/%d: %s" -msgstr "% d/%d: %s" - -msgctxt "#32611" -msgid "%d/%d: %s S%02dE%02d" -msgstr "% d/%d: %s S % 02dE % 02d" - -# Dropbox related messages -# empty strings from id 32612 to 32707 -msgctxt "#32708" -msgid "Dropbox Error" -msgstr "שגיאה בדרופבוקס" - -msgctxt "#32709" -msgid "Unable to upload" -msgstr "לא מצליח להעלות" - -msgctxt "#32710" -msgid "Unable to download" -msgstr "לא מצליח להוריד" - -msgctxt "#32711" -msgid "WatchedList: Dropbox-Import (Movies)" -msgstr "דרופבוקס-ייבוא (סרטים)" - -msgctxt "#32712" -msgid "WatchedList: Dropbox-Import (Episodes)" -msgstr "דרופבוקס-ייבוא (פרקים)" - -msgctxt "#32713" -msgid "Dropbox synchronization" -msgstr "סנכרון דרופבוקס" - -msgctxt "#32714" -msgid "Upload successfull" -msgstr "העלאה הסתיימה בהצלחה" - -msgctxt "#32715" -msgid "Empty authorization code in settings" -msgstr "קוד אישור ריק בהגדרות" - -msgctxt "#32716" -msgid "Update the Dropbox database: ..." -msgstr "עדכן את מסד הנתונים של דרופבוקס:..." - -msgctxt "#32717" -msgid "Dropbox database updated: %d entries." -msgstr "דרופבוקס - עדכון מסד הנתונים: %d ערכים." - -msgctxt "#32718" -msgid "WatchedList: Dropbox-Export (Movies)" -msgstr "דרופבוקס-ייצוא (סרטים)" - -msgctxt "#32719" -msgid "WatchedList: Dropbox-Export (Episodes)" -msgstr "דרופבוקס-ייצוא (פרקים)" - -msgctxt "#32720" -msgid "Error importing python Dropbox API. Disable function." -msgstr "שגיאת ייבוא מפתח פיתון Dropbox API. בטל את הפונקציה." diff --git a/service.watchedlist/resources/settings.xml b/service.watchedlist/resources/settings.xml deleted file mode 100644 index 3635d162f..000000000 --- a/service.watchedlist/resources/settings.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -