diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..1c9b0bde --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,283 @@ + + 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/addon.xml b/addon.xml new file mode 100644 index 00000000..6cb4f7ae --- /dev/null +++ b/addon.xml @@ -0,0 +1,25 @@ + + + + + + + executable video audio image + + + + + all + en + GNU GENERAL PUBLIC LICENSE. Version 2, June 1991 + + http://mediabrowser.tv/ + + + + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 00000000..b5806857 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,2 @@ +0.0.1 +- initital alpha version \ No newline at end of file diff --git a/default.py b/default.py new file mode 100644 index 00000000..73e54bc9 --- /dev/null +++ b/default.py @@ -0,0 +1,27 @@ +import xbmcaddon +import xbmcplugin +import xbmc +import xbmcgui +import os +import threading +import json +import urllib + +addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') +cwd = addonSettings.getAddonInfo('path') +BASE_RESOURCE_PATH = xbmc.translatePath( os.path.join( cwd, 'resources', 'lib' ) ) +sys.path.append(BASE_RESOURCE_PATH) + +WINDOW = xbmcgui.Window( 10000 ) + +import Utils as utils +from PlaybackUtils import PlaybackUtils + +# get the actions... +params=utils.get_params(sys.argv[2]) +mode = params.get('mode',"") +id = params.get('id',"") + +if mode == "play": + PlaybackUtils().PLAY(id) + diff --git a/fanart.jpg b/fanart.jpg new file mode 100644 index 00000000..77b142fe Binary files /dev/null and b/fanart.jpg differ diff --git a/icon.png b/icon.png new file mode 100644 index 00000000..e10aa896 Binary files /dev/null and b/icon.png differ diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 00000000..b93054b3 --- /dev/null +++ b/resources/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/resources/language/Dutch/strings.xml b/resources/language/Dutch/strings.xml new file mode 100644 index 00000000..a2630216 --- /dev/null +++ b/resources/language/Dutch/strings.xml @@ -0,0 +1,239 @@ + + + Primaire server adres: + Enkele mappen automatisch openen: + Afspelen van stream ipv SMB: + Log niveau: + Gebruikersnaam: + Wachtwoord: + Samba gebruikersnaam: + Samba wachtwoord: + Transcode: + Prestatie profilering inschakelen: + Lokale cache systeem + + MediaBrowser + Netwerk + Apparaatnaam + + Geavanceerd + Gebruikersnaam: + Wachtwoord: + Gebruik SIMPLEJSON ipv JSON + + Poortnummer: + Aantal recente films die getoond worden: + Aantal recente TV-series die getoond worden: + Aantal recente muziekalbums die getoond worden: + Markeer als bekeken bij starten van afspelen: + Gebruik seizoen poster bij afleveringen + + Genre filter ... + Speel alles vanaf hier + Vernieuwen + Wissen + Voeg film toe aan CouchPotato + + Ongeldige gebruikersnaam/wachtwoord + Gebruikersnaam niet gevonden + + Wissen... + Wacht op server voor wissen + + Server standaard + Titel + Jaar + Premiere datum + Datum toegevoegd + Critici beoordeling + Community beoordeling + Aantal keer bekeken + Budget + + Sorteer op + + Geen + Actie + Avontuur + Animatie + Misdaad + Comedy + Documentaire + Drama + Fantasie + Nederlands + Historie + Horror + Muziek + Musical + Mysterie + Romantiek + Science Fiction + Kort + Spanning + Thriller + Western + + Genre filter + Bevestig wissen + Dit item wissen ? Dit zal het bestand volledig verwijderen. + + Markeer als bekeken + Mark als onbekeken + Voeg toe aan favorieten + Verwijder uit favorieten + Sorteer op... + Sorteer oplopend + Sorteer aflopend + Toon acteurs + + + Hervatten + Hervatten vanaf + Start vanaf begin + + Interface + Inclusief stream info + Inclusief personen + Inclusief filminfo + Bij hervatten aantal seconden terugspringen + Markeer als bekeken wanneer na percentage gestopt + Inclusief aantal en afspeel tellers + - Achtergrond plaatjes verversen (secondes) + Inclusief hervat-percentage + Afleveringnummer tonen in titel + Toon voortgang + Laden van content + Retrieving Data + Parsing Jason Data + Downloading Jason Data + Done + Processing Item : + Toon wismogelijkheid na bekijken van aflevering + Afspeelfout! + Dit item kan niet worden afgespeeld + Lockaal pad gedetecteerd + De MB3 bibliotheek bevat lokale paden. U moet UNC-paden gebruiken of afspelen van stream inschakelen. Pad: + Waarschuwing + Debug logging ingeschakeld. + Dit heeft effect op de performance. + Fout + XBMB3C service werkt niet + Herstart XBMC aub + Zoeken + + Schakel Themamuziek in (vereist herstart) + - Herhalen van themamuziek + Activeer achtergrondafbeelding (vereist herstart) + Services + Activeer Info Loader (vereist herstart) + Activeer Menu Loader (vereist herstart) + Activeer WebSocket Remote (vereist herstart) + Activeer In Progress Loader (vereist herstart) + Activeer Recent Info Loader (vereist herstart) + Activeer Random Loader (vereist herstart) + Activeer Next Up Loader (vereist herstart) + + Skin ondersteund het vastleggen van views niet + Selecteer item actie (vereist herstart) + + Toon indactors + - Toon bekeken indator + - Toon aantal onbekeken indicator + - Toon afspeel-percentage indicator + Sorteer volgende (NextUp) op titel + Deactiveer speciale afbeeldingen (bv CoverArt) + Metadata + Afbeeldingen + Video kwaliteit + + Activeer Suggested Loader (vereist herstart) + Seizoen tonen in titel + Seizoenen verbergen + + Direct Play - HTTP + Direct Play + Transcoding + Server Detection Succeeded + Found server + Address : + + + Alle films + Alle TV + Alle Muziek + Kanalen + Recent toegevoegde films + Recent toegevoegde afleveringen + Recent toegevoegde albums + Niet afgekeken films + Niet afgekeken afleveringen + NextUp afleveringen + Favoriete films + Favoriete TV-series + Favoriete afleveringen + Vaak afgespeelde albums + Upcoming TV + BoxSets + Trailers + Muziek videos + Fotos + Onbekeken films + Film Genres + Film Studios + Film Acteurs + Onbekeken afleveringen + TV Genres + TV Networks + TV Acteurs + Afspeellijsten + Zoeken + Views instellen + + Selecteer gebruiker + Profilering ingeschakeld. + Svp onthouden om weer uit te schakelen na het testen. + Error in ArtworkRotationThread + Kan niet verbinden met server + Error in LoadMenuOptionsThread + + Activeer Playlists Loader (vereist herstart) + + Liedjes + Albums + Album artiesten + Artiesten + Muziek Genres + + Schakel Themavideos in (vereist herstart) + - Herhalen van themavideos + + Schakel het forceren van view uit + Schakel snelle modus in (beta) + Automatisch resterende afleveringen in een seizoen afspelen + Boxsets tonen in de overzichten (vereist herstart) + Afbeeldingen comprimeren + Activeer Skin Helper (vereust herstart) + Laatste + Bezig + Volgende + Gebruikerweergaven + + + Actief + Herstel standaard + Films + BoxSets + Trailers + Series + Seizoenen + Afleveringen + Muziek - artiesten + Muziek - albums + Muziekvideos + Muziek - liedjes + Kanalen + + + + \ No newline at end of file diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml new file mode 100644 index 00000000..00814d50 --- /dev/null +++ b/resources/language/English/strings.xml @@ -0,0 +1,252 @@ + + + Primary Server Address + Auto enter single folder items: + Play from HTTP instead of SMB: + Log Level: + Username: + Password: + Network Username: + Network Password: + Transcode: + Enable Performance Profiling + Local caching system + + MediaBrowser + Network + Device Name + + Advanced + Username: + Password: + Use SIMPLEJSON instead of JSON + + Port Number: + Number of recent Movies to show: + Number of recent TV episodes to show: + Number of recent Music Albums to show: + Mark watched at start of playback: + Set Season poster for episodes + + Genre Filter ... + Play All from Here + Refresh + Delete + Add Movie to CouchPotato + + Incorrect Username/Password + Username not found + + Deleting + Waiting for server to delete + + Server Default + Title + Year + Premiere Date + Date Created + Critic Rating + Community Rating + Play Count + Budget + + + Sort By + + None + Action + Adventure + Animation + Crime + Comedy + Documentary + Drama + Fantasy + Foreign + History + Horror + Music + Musical + Mystery + Romance + Science Fiction + Short + Suspense + Thriller + Western + + Genre Filter + Confirm file delete? + Delete this item? This action will delete media and associated data files. + + Mark Watched + Mark Unwatched + Add to Favorites + Remove from Favorites + Sort By ... + Sort Order Descending + Sort Order Ascending + Show People + + + Resume + Resume from + Start from beginning + + Interface + Include Stream Info + Include People + Include Overview + On Resume Jump Back Seconds + - Offer delete when stopped above % + Add Item and Played Counts + Background Art Refresh Rate (seconds) + Add Resume Percent + Add Episode Number + Show Load Progress + Loading Content + Retrieving Data + Parsing Jason Data + Downloading Jason Data + Done + Processing Item : + Offer delete for watched episodes + Play Error + This item is not playable + Local path detected + Your MB3 Server contains local paths. Please change server paths to UNC or change XBMB3C setting 'Play from Stream' to true. Path: + Warning + Debug logging enabled. + This will affect performance. + Error + Monitoring service is not running + If you have just installed please restart Kodi + Search + + Enable Theme Music (Requires Restart) + - Loop Theme Music + Enable Background Image (Requires Restart) + Services + Enable Info Loader (Requires Restart) + Enable Menu Loader (Requires Restart) + Enable WebSocket Remote (Requires Restart) + Enable In Progress Loader (Requires Restart) + Enable Recent Info Loader (Requires Restart) + Enable Random Loader (Requires Restart) + Enable Next Up Loader (Requires Restart) + + Skin does not support setting views + Select item action (Requires Restart) + + Show Indicators + - Show Watched Indicator + - Show Unplayed Count Indicator + - Show Played Percentage Indicator + Sort NextUp by Show Title + Disable Enhanced Images (eg CoverArt) + Metadata + Artwork + Video Quality + + Enable Suggested Loader (Requires Restart) + Add Season Number + Flatten Seasons + + Direct Play - HTTP + Direct Play + Transcoding + Server Detection Succeeded + Found server + Address : + + + All Movies + All TV + All Music + Channels + Recently Added Movies + Recently Added Episodes + Recently Added Albums + In Progress Movies + In Progress Episodes + Next Episodes + Favorite Movies + Favorite Shows + Favorite Episodes + Frequent Played Albums + Upcoming TV + BoxSets + Trailers + Music Videos + Photos + Unwatched Movies + Movie Genres + Movie Studios + Movie Actors + Unwatched Episodes + TV Genres + TV Networks + TV Actors + Playlists + Search + Set Views + + Select User + Profiling enabled. + Please remember to turn off when finished testing. + Error in ArtworkRotationThread + Unable to connect to server + Error in LoadMenuOptionsThread + + Enable Playlists Loader (Requires Restart) + + Songs + Albums + Album Artists + Artists + Music Genres + + Enable Theme Videos (Requires Restart) + - Loop Theme Videos + + Disable forced view + Enable Fast Processing + AutoPlay remaining episodes in a season + Show boxsets collapsed in views (Requires Restart) + Compress Artwork + Enable Skin Helper (Requires Restart) + Latest + In Progress + NextUp + User Views + Report Metrics + Use Kodi Sorting + Runtime + + Random Movies + Random Episodes + + Skin Compatibility Warning + Your current skin is not fully compatible. + For a better experience use a skin from the forum. + http://tinyurl.com/knfus2x + Don't Show Skin Compatibility Message + Add Show Name (Season + Episode) + + + Active + Clear Settings + Movies + BoxSets + Trailers + Series + Seasons + Episodes + Music Artists + Music Albums + Music Videos + Music Tracks + Channels + + + diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml new file mode 100644 index 00000000..1168bb8d --- /dev/null +++ b/resources/language/German/strings.xml @@ -0,0 +1,227 @@ + + + IP-Adresse des Servers + Automatisches Öffnen von Ordnern mit einem Eintrag + Via HTTP abspielen statt SMB/NFS: + Log Level: + Benutzername: + Passwort: + Samba-Benutzername: + Samba-Passwort: + Transkodieren: + Performancemessung aktivieren + Caching-Mechanismus + + MediaBrowser + Netzwerk + Gerätename + + Erweitert + Benutzername: + Passwort: + + Portnummer: + Anzahl der zuletzt hinzugefügten Filme: + Anzahl der zuletzt hinzugefügten Episoden: + Anzahl der zuletzt hinzugefügten Alben: + Bei Start der Wiedergabe als 'gesehen' markieren: + Staffelposter für Episoden nutzen + + Genre Filter ... + Alles von hier abspielen + Aktualisieren + Löschen + Film zu CouchPotato hinzufügen + + Benutzername/Passwort falsch + Benutzername nicht gefunden + + Lösche + Lösche von Server + + Server Standard + Titel + Jahr + Premierendatum + Datum hinzugefügt + Bewertung + Zuschauerbewertung + Abspielzähler + Budget + + Sortiere nach + + Kein Filter + Action + Adventure + Animation + Crime + Comedy + Documentary + Drama + Fantasy + Foreign + History + Horror + Music + Musical + Mystery + Romance + Science Fiction + Short + Suspense + Thriller + Western + + Genre Filter + Löschen von Dateien bestätigen? + Diesen Eintrag löschen? Diese Aktion löscht die Mediendatei und damit verbundene Daten. + + Als 'gesehen' markieren + Als 'ungesehen' markieren + Zu Favoriten hinzufügen + Von Favoriten entfernen + Sortiere nach ... + Sortierreihenfolge absteigend + Sortierreihenfolge aufsteigend + Zeige Mitwirkende + + + Fortsetzen + Fortsetzen bei + Am Anfang starten + + Benutzeroberfläche + Lade Streaminformationen + Lade Darsteller + Lade Inhaltsübersicht + Rücksprung bei Fortsetzen + Bei Stop nach x % als 'gesehen' markieren + Medien- und 'Abgespielt'-Zähler hinzufügen + - Aktualisierungsintervall von Hintergrundbildern (Sekunden) + Prozentanzeige für Fortsetzen + Episodennummer hinzufügen + Ladefortschritt anzeigen + Lade Inhalt + Lade Daten + Verarbeite Json Daten + Lade Json Daten + Fertig + Verarbeite Eintrag : + Löschen von gesehenen Episoden anbieten + Abspielfehler + Dieser Eintrag ist nicht abspielbar + Lokaler Pfad erkannt + Warnung + Debug Logging aktiviert. + Dies beeinträchtigt die Performance. + Fehler + XBMB3C-Service läuft nicht + Bitte XBMC neustarten + Suche + + Themen-Musik aktivieren (Erfordert Neustart) + - Themen-Musik in Schleife abspielen + Laden im Hintergrund aktivieren (Erfordert Neustart) + Dienste + Info-Loader aktivieren (Erfordert Neustart) + Menü-Loader aktivieren (Erfordert Neustart) + WebSocket Fernbedienung aktivieren (Erfordert Neustart) + 'Laufende Medien'-Loader aktivieren (Erfordert Neustart) + 'Zuletzt hinzugefügt' Loader aktivieren (Erfordert Neustart) + 'Zufallsmedien'-Loader aktivieren (Erfordert Neustart) + 'Nächste'-Loader aktivieren (Erfordert Neustart) + + Skin unterstützt das Setzen von Views nicht + Aktion bei Auswahl (Erfordert Neustart) + + Indikatoren + - 'Gesehen'-Indikator anzeigen + - Zähler für ungesehene Medien anzeigen + - Abspiel-Prozentanzeige aktivieren + Sortiere 'Nächste' nach Serientitel + Deaktiviere erweiterte Bilder (z.B. CoverArt) + Metadaten + Grafiken + Videoqualität + + 'Empfohlen'-Loader aktivieren (Erfordert Neustart) + Staffelnummer hinzufügen + Serienstaffeln reduzieren + Direkte Wiedergabe - HTTP + Direkte Wiedergabe + Transkodierung + Serversuche erfolgreich + Server gefunden + Addresse : + + Alle Filme + Alle Serien + Alles an Musik + Kanäle + Zuletzt hinzugefügte Filme + Zuletzt hinzugefügte Episoden + Zuletzt hinzugefügte Alben + Begonnene Filme + Begonnene Episoden + Nächste Episoden + Favorisierte Filme + Favorisierte Serien + Favorisierte Episoden + Häufig gespielte Alben + Anstehende Serien + Sammlungen + Trailer + Musikvideos + Fotos + Ungesehene Filme + Filmgenres + Studios + Filmdarsteller + Ungesehene Episoden + Seriengenres + Fernsehsender + Seriendarsteller + Wiedergabelisten + Suche + Ansichten festlegen + + Wähle Benutzer + Messung aktiviert. + Bitte daran denken, nach dem Testen wieder zu deaktivieren. + Fehler in ArtworkRotationThread + Verbindung zum Server fehlgeschlagen + Fehler in LoadMenuOptionsThread + + 'Playlist'-Loader aktivieren (Erfordert Neustart) + + Songs + Alben + Album-Interpreten + Interpreten + Musik-Genres + + Themen-Videos aktivieren (Erfordert Neustart) + - Themen-Videos in Schleife abspielen + + Festgelegte Ansichten deaktivieren + Schnelleres Laden der Daten aktivieren + Spiele weitere Episoden einer Staffel automatisch ab + Aktiviere gruppierte Darstellung von Sammlungen (Erfordert Neustart) + Bilder komprimieren + + + Aktiviert + Zurücksetzen + Filme + BoxSets + Trailer + Serien + Staffeln + Episoden + Interpreten + Alben + Musikvideos + Musikstücke + + diff --git a/resources/lib/API.py b/resources/lib/API.py new file mode 100644 index 00000000..db796242 --- /dev/null +++ b/resources/lib/API.py @@ -0,0 +1,250 @@ +# API.py +# This class helps translate more complex cases from the MediaBrowser API to the XBMC API + +from datetime import datetime + +class API(): + + def getPeople(self, item): + # Process People + director='' + writer='' + cast=[] + people = item.get("People") + if(people != None): + for person in people: + if(person.get("Type") == "Director"): + director = director + person.get("Name") + ' ' + if(person.get("Type") == "Writing"): + writer = person.get("Name") + if(person.get("Type") == "Writer"): + writer = person.get("Name") + if(person.get("Type") == "Actor"): + Name = person.get("Name") + Role = person.get("Role") + if Role == None: + Role = '' + cast.append(Name) + return {'Director' : director, + 'Writer' : writer, + 'Cast' : cast + } + + def getTimeInfo(self, item): + resumeTime = '' + userData = item.get("UserData") + PlaybackPositionTicks = '100' + if userData.get("PlaybackPositionTicks") != None: + PlaybackPositionTicks = str(userData.get("PlaybackPositionTicks")) + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + resumeTime = reasonableTicks / 10000 + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + except TypeError: + tempDuration = "0" + cappedPercentage = None + resume=0 + percentage=0 + if (resumeTime != "" and int(resumeTime) > 0): + duration = float(tempDuration) + if(duration > 0): + resume = float(resumeTime) / 60 + percentage = int((resume / duration) * 100.0) + return {'Duration' : tempDuration, + 'TotalTime' : tempDuration, + 'Percent' : str(percentage), + 'ResumeTime' : str(resume) + } + + def getStudio(self, item): + # Process Studio + studio = "" + if item.get("SeriesStudio") != None and item.get("SeriesStudio") != '': + studio = item.get("SeriesStudio") + if studio == "": + studios = item.get("Studios") + if(studios != None): + for studio_string in studios: + if studio=="": #Just take the first one + temp=studio_string.get("Name") + studio=temp.encode('utf-8') + return studio + + def getMediaStreams(self, item, mediaSources=False): + # Process MediaStreams + channels = '' + videocodec = '' + audiocodec = '' + height = '' + width = '' + aspectratio = '1:1' + aspectfloat = 1.85 + + if mediaSources == True: + mediaSources = item.get("MediaSources") + if(mediaSources != None): + MediaStreams = mediaSources[0].get("MediaStreams") + else: + MediaStreams = None + else: + MediaStreams = item.get("MediaStreams") + if(MediaStreams != None): + #mediaStreams = MediaStreams[0].get("MediaStreams") + if(MediaStreams != None): + for mediaStream in MediaStreams: + if(mediaStream.get("Type") == "Video"): + videocodec = mediaStream.get("Codec") + height = str(mediaStream.get("Height")) + width = str(mediaStream.get("Width")) + aspectratio = mediaStream.get("AspectRatio") + if aspectratio != None and len(aspectratio) >= 3: + try: + aspectwidth,aspectheight = aspectratio.split(':') + aspectfloat = float(aspectwidth) / float(aspectheight) + except: + aspectfloat = 1.85 + if(mediaStream.get("Type") == "Audio"): + audiocodec = mediaStream.get("Codec") + channels = mediaStream.get("Channels") + return {'channels' : str(channels), + 'videocodec' : videocodec, + 'audiocodec' : audiocodec, + 'height' : height, + 'width' : width, + 'aspectratio' : str(aspectfloat) + } + + def getUserData(self, item): + userData = item.get("UserData") + resumeTime = 0 + if(userData != None): + if userData.get("Played") != True: + watched="True" + else: + watched="False" + if userData.get("IsFavorite") == True: + favorite="True" + else: + favorite="False" + if(userData.get("Played") == True): + playcount="1" + else: + playcount="0" + if userData.get('UnplayedItemCount') != None: + UnplayedItemCount = userData.get('UnplayedItemCount') + else: + UnplayedItemCount = "0" + if userData.get('PlaybackPositionTicks') != None: + PlaybackPositionTicks = userData.get('PlaybackPositionTicks') + else: + PlaybackPositionTicks = '' + return {'Watched' : watched, + 'Favorite' : favorite, + 'PlayCount': playcount, + 'UnplayedItemCount' : UnplayedItemCount, + 'PlaybackPositionTicks' : str(PlaybackPositionTicks) + } + + def getGenre(self,item): + genre = "" + genres = item.get("Genres") + if genres != None and genres != []: + for genre_string in genres: + if genre == "": #Just take the first genre + genre = genre_string + else: + genre = genre + " / " + genre_string + elif item.get("SeriesGenres") != None and item.get("SeriesGenres") != '': + genres = item.get("SeriesGenres") + if genres != None and genres != []: + for genre_string in genres: + if genre == "": #Just take the first genre + genre = genre_string + else: + genre = genre + " / " + genre_string + return genre + + def getName(self, item): + Temp = item.get("Name") + if Temp == None: + Temp = "" + Name=Temp.encode('utf-8') + return Name + + def getRecursiveItemCount(self, item): + if item.get("RecursiveItemCount") != None: + return str(item.get("RecursiveItemCount")) + else: + return "0" + + def getSeriesName(self, item): + Temp = item.get("SeriesName") + if Temp == None: + Temp = "" + Name=Temp.encode('utf-8') + return Name + + def getOverview(self, item): + Temp = item.get("Overview") + if Temp == None: + Temp='' + Overview1=Temp.encode('utf-8') + Overview=str(Overview1) + return Overview + + def getPremiereDate(self, item): + if(item.get("PremiereDate") != None): + premieredatelist = (item.get("PremiereDate")).split("T") + premieredate = premieredatelist[0] + else: + premieredate = "" + Temp = premieredate + premieredate = Temp.encode('utf-8') + return premieredate + + def getTVInfo(self, item, userData): + TotalSeasons = 0 if item.get("ChildCount")==None else item.get("ChildCount") + TotalEpisodes = 0 if item.get("RecursiveItemCount")==None else item.get("RecursiveItemCount") + WatchedEpisodes = 0 if userData.get("UnplayedItemCount")==None else TotalEpisodes-int(userData.get("UnplayedItemCount")) + UnWatchedEpisodes = 0 if userData.get("UnplayedItemCount")==None else int(userData.get("UnplayedItemCount")) + NumEpisodes = TotalEpisodes + tempEpisode = "" + if (item.get("IndexNumber") != None): + episodeNum = item.get("IndexNumber") + if episodeNum < 10: + tempEpisode = "0" + str(episodeNum) + else: + tempEpisode = str(episodeNum) + + tempSeason = "" + if (str(item.get("ParentIndexNumber")) != None): + tempSeason = str(item.get("ParentIndexNumber")) + if item.get("ParentIndexNumber") < 10: + tempSeason = "0" + tempSeason + if item.get("SeriesName") != None: + temp=item.get("SeriesName") + SeriesName=temp.encode('utf-8') + else: + SeriesName='' + return {'TotalSeasons' : str(TotalSeasons), + 'TotalEpisodes' : str(TotalEpisodes), + 'WatchedEpisodes' : str(WatchedEpisodes), + 'UnWatchedEpisodes': str(UnWatchedEpisodes), + 'NumEpisodes' : str(NumEpisodes), + 'Season' : tempSeason, + 'Episode' : tempEpisode, + 'SeriesName' : SeriesName + } + def getDate(self, item): + tempDate = item.get("DateCreated") + if tempDate != None: + tempDate = tempDate.split("T")[0] + date = tempDate.split("-") + tempDate = date[2] + "." + date[1] + "." +date[0] + else: + tempDate = "01.01.2000" + return tempDate \ No newline at end of file diff --git a/resources/lib/ClientInformation.py b/resources/lib/ClientInformation.py new file mode 100644 index 00000000..915e67e9 --- /dev/null +++ b/resources/lib/ClientInformation.py @@ -0,0 +1,52 @@ +from uuid import uuid4 as uuid4 +import xbmc +import xbmcaddon +import xbmcgui + + +class ClientInformation(): + + def getMachineId(self): + + WINDOW = xbmcgui.Window( 10000 ) + + clientId = WINDOW.getProperty("client_id") + self.addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + if(clientId == None or clientId == ""): + xbmc.log("CLIENT_ID - > No Client ID in WINDOW") + clientId = self.addonSettings.getSetting('client_id') + + if(clientId == None or clientId == ""): + xbmc.log("CLIENT_ID - > No Client ID in SETTINGS") + uuid = uuid4() + clientId = str("%012X" % uuid) + WINDOW.setProperty("client_id", clientId) + self.addonSettings.setSetting('client_id', clientId) + xbmc.log("CLIENT_ID - > New Client ID : " + clientId) + else: + WINDOW.setProperty('client_id', clientId) + xbmc.log("CLIENT_ID - > Client ID saved to WINDOW from Settings : " + clientId) + + return clientId + + def getVersion(self): + version = xbmcaddon.Addon(id="plugin.video.mb3sync").getAddonInfo("version") + return version + + + def getPlatform(self): + + if xbmc.getCondVisibility('system.platform.osx'): + return "OSX" + elif xbmc.getCondVisibility('system.platform.atv2'): + return "ATV2" + elif xbmc.getCondVisibility('system.platform.ios'): + return "iOS" + elif xbmc.getCondVisibility('system.platform.windows'): + return "Windows" + elif xbmc.getCondVisibility('system.platform.linux'): + return "Linux/RPi" + elif xbmc.getCondVisibility('system.platform.android'): + return "Linux/Android" + + return "Unknown" diff --git a/resources/lib/ConnectionManager.py b/resources/lib/ConnectionManager.py new file mode 100644 index 00000000..6a9b5995 --- /dev/null +++ b/resources/lib/ConnectionManager.py @@ -0,0 +1,147 @@ +################################################################################################# +# connection manager class +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +from DownloadUtils import DownloadUtils +import urllib +import sys +import socket + +#define our global download utils +logLevel = 1 +########################################################################### +class ConnectionManager(): + + addonSettings = None + __addon__ = xbmcaddon.Addon(id='plugin.video.mb3sync') + __addondir__ = xbmc.translatePath( __addon__.getAddonInfo('profile') ) + __language__ = __addon__.getLocalizedString + + def printDebug(self, msg, level = 1): + if(logLevel >= level): + if(logLevel == 2): + try: + xbmc.log("mb3sync " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg)) + except UnicodeEncodeError: + xbmc.log("mb3sync " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) + else: + try: + xbmc.log("mb3sync " + str(level) + " -> " + str(msg)) + except UnicodeEncodeError: + xbmc.log("mb3sync " + str(level) + " -> " + str(msg.encode('utf-8'))) + + def checkServer(self): + + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("Server_Checked", "True") + + self.printDebug ("mb3sync Connection Manager Called") + self.addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + port = self.addonSettings.getSetting('port') + host = self.addonSettings.getSetting('ipaddress') + + if(len(host) != 0 and host != ""): + self.printDebug ("mb3sync server already set") + return + + serverInfo = self.getServerDetails() + + if(serverInfo == None): + self.printDebug ("mb3sync getServerDetails failed") + return + + index = serverInfo.find(":") + + if(index <= 0): + self.printDebug ("mb3sync getServerDetails data not correct : " + serverInfo) + return + + server_address = serverInfo[:index] + server_port = serverInfo[index+1:] + self.printDebug ("mb3sync detected server info " + server_address + " : " + server_port) + + xbmcgui.Dialog().ok(self.__language__(30167), self.__language__(30168), self.__language__(30169) + server_address, self.__language__(30030) + server_port) + + # get a list of users + self.printDebug ("Getting user list") + jsonData = None + downloadUtils = DownloadUtils() + try: + jsonData = downloadUtils.downloadUrl(server_address + ":" + server_port + "/mediabrowser/Users/Public?format=json") + except Exception, msg: + error = "Get User unable to connect to " + server_address + ":" + server_port + " : " + str(msg) + xbmc.log (error) + return "" + + if(jsonData == False): + return + + self.printDebug("jsonData : " + str(jsonData), level=2) + result = json.loads(jsonData) + + names = [] + userList = [] + for user in result: + name = user.get("Name") + userList.append(name) + if(user.get("HasPassword") == True): + name = name + " (Secure)" + names.append(name) + + self.printDebug ("User List : " + str(names)) + self.printDebug ("User List : " + str(userList)) + return_value = xbmcgui.Dialog().select(self.__language__(30200), names) + + if(return_value > -1): + selected_user = userList[return_value] + self.printDebug("Setting Selected User : " + selected_user) + if self.addonSettings.getSetting("port") != server_port: + self.addonSettings.setSetting("port", server_port) + if self.addonSettings.getSetting("ipaddress") != server_address: + self.addonSettings.setSetting("ipaddress", server_address) + if self.addonSettings.getSetting("username") != selected_user: + self.addonSettings.setSetting("username", selected_user) + + def getServerDetails(self): + + self.printDebug("Getting Server Details from Network") + + MESSAGE = "who is MediaBrowserServer?" + #MULTI_GROUP = ("224.3.29.71", 7359) + #MULTI_GROUP = ("127.0.0.1", 7359) + MULTI_GROUP = ("", 7359) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(6.0) + + #ttl = struct.pack('b', 20) + #sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) + + xbmc.log("MutliGroup : " + str(MULTI_GROUP)); + xbmc.log("Sending UDP Data : " + MESSAGE); + sock.sendto(MESSAGE, MULTI_GROUP) + + try: + data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes + xbmc.log("Received Response : " + data) + if(data[0:18] == "MediaBrowserServer"): + xbmc.log("Found Server : " + data[19:]) + return data[19:] + except: + xbmc.log("No UDP Response") + pass + + return None + diff --git a/resources/lib/DownloadUtils.py b/resources/lib/DownloadUtils.py new file mode 100644 index 00000000..b1974e1e --- /dev/null +++ b/resources/lib/DownloadUtils.py @@ -0,0 +1,594 @@ +import xbmc +import xbmcgui +import xbmcaddon +import urllib +import urllib2 +import httplib +import hashlib +import StringIO +import gzip +import sys +import inspect +import json as json +from random import randrange +from uuid import uuid4 as uuid4 +from ClientInformation import ClientInformation +import Utils as utils +import encodings +import time +import traceback + +addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') +getString = addonSettings.getLocalizedString + +class DownloadUtils(): + + logLevel = 0 + getString = None + LogCalls = False + TrackLog = "" + TotalUrlCalls = 0 + + def __init__(self, *args): + pass + + def getServer(self): + port = addonSettings.getSetting('port') + host = addonSettings.getSetting('ipaddress') + return host + ":" + port + + def getUserId(self, suppress=True): + + WINDOW = xbmcgui.Window( 10000 ) + port = addonSettings.getSetting('port') + host = addonSettings.getSetting('ipaddress') + userName = addonSettings.getSetting('username') + + userid = WINDOW.getProperty("userid" + userName) + + if(userid != None and userid != ""): + utils.logMsg("MB3 Sync","DownloadUtils -> Returning saved UserID : " + userid + "UserName: " + userName) + return userid + + utils.logMsg("MB3 Sync","Looking for user name: " + userName) + + authOk = self.authenticate() + if(authOk == ""): + if(suppress == False): + xbmcgui.Dialog().ok(getString(30044), getString(30044)) + return "" + + userid = WINDOW.getProperty("userid"+ userName) + if(userid == "" and suppress == False): + xbmcgui.Dialog().ok(getString(30045),getString(30045)) + + utils.logMsg("MB3 Sync","userid : " + userid) + self.postcapabilities() + + return userid + + def postcapabilities(self): + utils.logMsg("MB3 Sync","postcapabilities called") + + # Set Capabilities + mb3Port = addonSettings.getSetting('port') + mb3Host = addonSettings.getSetting('ipaddress') + clientInfo = ClientInformation() + machineId = clientInfo.getMachineId() + + # get session id + url = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Sessions?DeviceId=" + machineId + "&format=json" + utils.logMsg("MB3 Sync","Session URL : " + url); + jsonData = self.downloadUrl(url) + utils.logMsg("MB3 Sync","Session JsonData : " + jsonData) + result = json.loads(jsonData) + utils.logMsg("MB3 Sync","Session JsonData : " + str(result)) + sessionId = result[0].get("Id") + utils.logMsg("MB3 Sync","Session Id : " + str(sessionId)) + + # post capability data + playableMediaTypes = "Audio,Video,Photo" + supportedCommands = "Play,Playstate,DisplayContent,GoHome,SendString,GoToSettings,DisplayMessage,PlayNext" + + url = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Sessions/Capabilities?Id=" + sessionId + "&PlayableMediaTypes=" + playableMediaTypes + "&SupportedCommands=" + supportedCommands + + postData = {} + #postData["Id"] = sessionId; + #postData["PlayableMediaTypes"] = "Video"; + #postData["SupportedCommands"] = "MoveUp"; + stringdata = json.dumps(postData) + utils.logMsg("MB3 Sync","Capabilities URL : " + url); + utils.logMsg("MB3 Sync","Capabilities Data : " + stringdata) + + self.downloadUrl(url, postBody=stringdata, type="POST") + + def authenticate(self): + WINDOW = xbmcgui.Window( 10000 ) + token = WINDOW.getProperty("AccessToken"+addonSettings.getSetting('username')) + if(token != None and token != ""): + utils.logMsg("MB3 Sync","DownloadUtils -> Returning saved AccessToken for user : " + addonSettings.getSetting('username') + " token: "+ token) + return token + + port = addonSettings.getSetting("port") + host = addonSettings.getSetting("ipaddress") + if(host == None or host == "" or port == None or port == ""): + return "" + + url = "http://" + addonSettings.getSetting("ipaddress") + ":" + addonSettings.getSetting("port") + "/mediabrowser/Users/AuthenticateByName?format=json" + + clientInfo = ClientInformation() + txt_mac = clientInfo.getMachineId() + version = clientInfo.getVersion() + + deviceName = addonSettings.getSetting('deviceName') + deviceName = deviceName.replace("\"", "_") + + authString = "Mediabrowser Client=\"Kodi\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" + headers = {'Accept-encoding': 'gzip', 'Authorization' : authString} + + if addonSettings.getSetting('password') !=None and addonSettings.getSetting('password') !='': + sha1 = hashlib.sha1(addonSettings.getSetting('password')) + sha1 = sha1.hexdigest() + else: + sha1 = 'da39a3ee5e6b4b0d3255bfef95601890afd80709' + + messageData = "username=" + addonSettings.getSetting('username') + "&password=" + sha1 + + resp = self.downloadUrl(url, postBody=messageData, type="POST", authenticate=False, suppress=True) + + result = None + accessToken = None + try: + result = json.loads(resp) + accessToken = result.get("AccessToken") + except: + pass + + if(result != None and accessToken != None): + utils.logMsg("MB3 Sync","User Authenticated : " + accessToken) + WINDOW.setProperty("AccessToken"+addonSettings.getSetting('username'), accessToken) + WINDOW.setProperty("userid"+addonSettings.getSetting('username'), result.get("User").get("Id")) + WINDOW.setProperty("mb3_authenticated", "true") + return accessToken + else: + utils.logMsg("MB3 Sync","User NOT Authenticated") + WINDOW.setProperty("AccessToken"+addonSettings.getSetting('username'), "") + WINDOW.setProperty("mb3_authenticated", "false") + return "" + + def getArtwork(self, data, type, index = "0", userParentInfo = False): + + id = data.get("Id") + getSeriesData = False + userData = data.get("UserData") + + if type == "tvshow.poster": # Change the Id to the series to get the overall series poster + if data.get("Type") == "Season" or data.get("Type")== "Episode": + id = data.get("SeriesId") + getSeriesData = True + elif type == "poster" and data.get("Type") == "Episode" and addonSettings.getSetting('useSeasonPoster')=='true': # Change the Id to the Season to get the season poster + id = data.get("SeasonId") + if type == "poster" or type == "tvshow.poster": # Now that the Ids are right, change type to MB3 name + type="Primary" + if data.get("Type") == "Season": # For seasons: primary (poster), thumb and banner get season art, rest series art + if type != "Primary" and type != "Primary2" and type != "Primary3" and type != "Primary4" and type != "Thumb" and type != "Banner" and type!="Thumb3": + id = data.get("SeriesId") + getSeriesData = True + if data.get("Type") == "Episode": # For episodes: primary (episode thumb) gets episode art, rest series art. + if type != "Primary" and type != "Primary2" and type != "Primary3" and type != "Primary4": + id = data.get("SeriesId") + getSeriesData = True + if type =="Primary2" or type=="Primary3" or type=="Primary4": + id = data.get("SeasonId") + getSeriesData = True + if data.get("SeasonUserData") != None: + userData = data.get("SeasonUserData") + if id == None: + id=data.get("Id") + + imageTag = "e3ab56fe27d389446754d0fb04910a34" # a place holder tag, needs to be in this format + originalType = type + if type == "Primary2" or type == "Primary3" or type == "Primary4" or type=="SeriesPrimary": + type = "Primary" + if type == "Backdrop2" or type=="Backdrop3" or type=="BackdropNoIndicators": + type = "Backdrop" + if type == "Thumb2" or type=="Thumb3": + type = "Thumb" + if(data.get("ImageTags") != None and data.get("ImageTags").get(type) != None): + imageTag = data.get("ImageTags").get(type) + + if (data.get("Type") == "Episode" or data.get("Type") == "Season") and type=="Logo": + imageTag = data.get("ParentLogoImageTag") + if (data.get("Type") == "Episode" or data.get("Type") == "Season") and type=="Art": + imageTag = data.get("ParentArtImageTag") + if (data.get("Type") == "Episode") and originalType=="Thumb3": + imageTag = data.get("SeriesThumbImageTag") + if (data.get("Type") == "Season") and originalType=="Thumb3" and imageTag=="e3ab56fe27d389446754d0fb04910a34" : + imageTag = data.get("ParentThumbImageTag") + id = data.get("SeriesId") + + query = "" + height = "10000" + width = "10000" + played = "0" + totalbackdrops = 0 + + if addonSettings.getSetting('showArtIndicators')=='true': # add watched, unplayedcount and percentage played indicators to posters + if (originalType =="Primary" or originalType =="Backdrop" or originalType =="Banner") and data.get("Type") != "Episode": + if originalType =="Backdrop" and index == "0" and data.get("BackdropImageTags") != None: + totalbackdrops = len(data.get("BackdropImageTags")) + if totalbackdrops != 0: + index = str(randrange(0,totalbackdrops)) + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + + if(userData != None and userData.get("Played") == True and addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + elif originalType =="Primary2": + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "338" + width = "226" + + elif originalType =="Primary3" or originalType == "SeriesPrimary": + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + + + elif originalType =="Primary4": + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "270" + width = "180" + + elif type =="Primary" and data.get("Type") == "Episode": + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "410" + width = "770" + + + elif originalType =="Backdrop2" or originalType =="Thumb2" and data.get("Type") != "Episode": + if originalType =="Backdrop2" and data.get("BackdropImageTags") != None: + totalbackdrops = len(data.get("BackdropImageTags")) + if totalbackdrops != 0: + index = str(randrange(0,totalbackdrops)) + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "370" + width = "660" + + elif originalType =="Backdrop3" or originalType =="Thumb3" and data.get("Type") != "Episode": + if originalType =="Backdrop3" and data.get("BackdropImageTags") != None: + totalbackdrops = len(data.get("BackdropImageTags")) + if totalbackdrops != 0: + index = str(randrange(0,totalbackdrops)) + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "910" + width = "1620" + + if originalType =="BackdropNoIndicators" and index == "0" and data.get("BackdropImageTags") != None: + totalbackdrops = len(data.get("BackdropImageTags")) + if totalbackdrops != 0: + index = str(randrange(0,totalbackdrops)) + # use the local image proxy server that is made available by this addons service + + port = addonSettings.getSetting('port') + host = addonSettings.getSetting('ipaddress') + server = host + ":" + port + + if addonSettings.getSetting('compressArt')=='true': + query = query + "&Quality=90" + + if imageTag == None: + imageTag = "e3ab56fe27d389446754d0fb04910a34" + artwork = "http://" + server + "/mediabrowser/Items/" + str(id) + "/Images/" + type + "/" + index + "/" + imageTag + "/original/" + width + "/" + height + "/" + played + "?" + query + if addonSettings.getSetting('disableCoverArt')=='true': + artwork = artwork + "&EnableImageEnhancers=false" + + utils.logMsg("MB3 Sync","getArtwork : " + artwork, level=2) + + # do not return non-existing images + if ( (type!="Backdrop" and imageTag=="e3ab56fe27d389446754d0fb04910a34") | #Remember, this is the placeholder tag, meaning we didn't find a valid tag + (type=="Backdrop" and data.get("BackdropImageTags") != None and len(data.get("BackdropImageTags")) == 0) | + (type=="Backdrop" and data.get("BackdropImageTag") != None and len(data.get("BackdropImageTag")) == 0) + ): + if type != "Backdrop" or (type=="Backdrop" and getSeriesData==True and data.get("ParentBackdropImageTags") == None) or (type=="Backdrop" and getSeriesData!=True): + artwork='' + + return artwork + + def getUserArtwork(self, data, type, index = "0"): + + id = data.get("Id") + + port = addonSettings.getSetting('port') + host = addonSettings.getSetting('ipaddress') + server = host + ":" + port + + artwork = "http://" + server + "/mediabrowser/Users/" + str(id) + "/Images/" + type + "?Format=original" + + return artwork + + def imageUrl(self, id, type, index, width, height): + + port = addonSettings.getSetting('port') + host = addonSettings.getSetting('ipaddress') + server = host + ":" + port + + return "http://" + server + "/mediabrowser/Items/" + str(id) + "/Images/" + type + "/" + str(index) + "/e3ab56fe27d389446754d0fb04910a34/original/" + str(width) + "/" + str(height) + "/0" + + def getAuthHeader(self, authenticate=True): + clientInfo = ClientInformation() + txt_mac = clientInfo.getMachineId() + version = clientInfo.getVersion() + + deviceName = addonSettings.getSetting('deviceName') + deviceName = deviceName.replace("\"", "_") + + if(authenticate == False): + authString = "MediaBrowser Client=\"Kodi\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" + headers = {"Accept-encoding": "gzip", "Accept-Charset" : "UTF-8,*", "Authorization" : authString} + return headers + else: + userid = self.getUserId() + authString = "MediaBrowser UserId=\"" + userid + "\",Client=\"Kodi\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" + headers = {"Accept-encoding": "gzip", "Accept-Charset" : "UTF-8,*", "Authorization" : authString} + + authToken = self.authenticate() + if(authToken != ""): + headers["X-MediaBrowser-Token"] = authToken + + utils.logMsg("MB3 Sync","Authentication Header : " + str(headers)) + return headers + + def downloadUrl(self, url, suppress=False, postBody=None, type="GET", popup=0, authenticate=True ): + utils.logMsg("MB3 Sync","== ENTER: getURL ==") + + self.TotalUrlCalls = self.TotalUrlCalls + 1 + if(self.LogCalls): + stackString = "" + for f in inspect.stack(): + stackString = stackString + "\r - " + str(f) + self.TrackLog = self.TrackLog + "HTTP_API_CALL : " + url + stackString + "\r" + + link = "" + try: + if url[0:4] == "http": + serversplit = 2 + urlsplit = 3 + else: + serversplit = 0 + urlsplit = 1 + + server = url.split('/')[serversplit] + urlPath = "/"+"/".join(url.split('/')[urlsplit:]) + + utils.logMsg("MB3 Sync","DOWNLOAD_URL = " + url) + utils.logMsg("MB3 Sync","server = "+str(server), level=2) + utils.logMsg("MB3 Sync","urlPath = "+str(urlPath), level=2) + + conn = httplib.HTTPConnection(server, timeout=5) + + head = self.getAuthHeader(authenticate) + utils.logMsg("MB3 Sync","HEADERS : " + str(head), level=1) + + # make the connection and send the request + if(postBody != None): + head["Content-Type"] = "application/x-www-form-urlencoded" + head["Content-Length"] = str(len(postBody)) + utils.logMsg("MB3 Sync","POST DATA : " + postBody) + conn.request(method=type, url=urlPath, body=postBody, headers=head) + else: + conn.request(method=type, url=urlPath, headers=head) + + # get the response + tries = 0 + while tries <= 4: + try: + data = conn.getresponse() + break + except: + # TODO: we need to work out which errors we can just quit trying immediately + if(xbmc.abortRequested == True): + return "" + xbmc.sleep(100) + if(xbmc.abortRequested == True): + return "" + tries += 1 + if tries == 5: + data = conn.getresponse() + + utils.logMsg("MB3 Sync","GET URL HEADERS : " + str(data.getheaders()), level=2) + + # process the response + contentType = "none" + if int(data.status) == 200: + retData = data.read() + contentType = data.getheader('content-encoding') + utils.logMsg("MB3 Sync","Data Len Before : " + str(len(retData)), level=2) + if(contentType == "gzip"): + retData = StringIO.StringIO(retData) + gzipper = gzip.GzipFile(fileobj=retData) + link = gzipper.read() + else: + link = retData + utils.logMsg("MB3 Sync","Data Len After : " + str(len(link)), level=2) + utils.logMsg("MB3 Sync","====== 200 returned =======", level=2) + utils.logMsg("MB3 Sync","Content-Type : " + str(contentType), level=2) + utils.logMsg("MB3 Sync",link, level=2) + utils.logMsg("MB3 Sync","====== 200 finished ======", level=2) + + elif ( int(data.status) == 301 ) or ( int(data.status) == 302 ): + try: + conn.close() + except: + pass + return data.getheader('Location') + + elif int(data.status) == 401: + error = "HTTP response error: " + str(data.status) + " " + str(data.reason) + xbmc.log(error) + + WINDOW = xbmcgui.Window(10000) + timeStamp = WINDOW.getProperty("mb3sync_LAST_USER_ERROR") + if(timeStamp == None or timeStamp == ""): + timeStamp = "0" + + if((int(timeStamp) + 10) < int(time.time())): + xbmcgui.Dialog().ok(getString(30135), getString(30044)) + WINDOW.setProperty("mb3sync_LAST_USER_ERROR", str(int(time.time()))) + + try: + conn.close() + except: + pass + return "" + + elif int(data.status) >= 400: + error = "HTTP response error: " + str(data.status) + " " + str(data.reason) + xbmc.log(error) + if suppress is False: + if popup == 0: + xbmc.executebuiltin("XBMC.Notification(URL error: "+ str(data.reason) +",)") + else: + xbmcgui.Dialog().ok(getString(30135),server) + try: + conn.close() + except: + pass + return "" + else: + link = "" + except Exception, msg: + error = "Unable to connect to " + str(server) + " : " + str(msg) + xbmc.log(error) + stack = self.FormatException() + utils.logMsg("MB3 Sync",stack) + if suppress is False: + if popup == 0: + xbmc.executebuiltin("XBMC.Notification(: Connection Error: Error connecting to server,)") + else: + xbmcgui.Dialog().ok(getString(30204), str(msg)) + pass + else: + try: + conn.close() + except: + pass + + return link + + def FormatException(self): + exception_list = traceback.format_stack() + exception_list = exception_list[:-2] + exception_list.extend(traceback.format_tb(sys.exc_info()[2])) + exception_list.extend(traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])) + + exception_str = "Traceback (most recent call last):\n" + exception_str += "".join(exception_list) + # Removing the last \n + exception_str = exception_str[:-1] + + return exception_str + + def __del__(self): + return + # xbmc.log("\rURL_REQUEST_REPORT : Total Calls : " + str(self.TotalUrlCalls) + "\r" + self.TrackLog) diff --git a/resources/lib/KodiMonitor.py b/resources/lib/KodiMonitor.py new file mode 100644 index 00000000..df386390 --- /dev/null +++ b/resources/lib/KodiMonitor.py @@ -0,0 +1,40 @@ +################################################################################################# +# Kodi Monitor +# Watched events that occur in Kodi, like setting media watched +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon +import json + +import Utils as utils +from LibrarySync import LibrarySync + +librarySync = LibrarySync() + +WINDOW = xbmcgui.Window( 10000 ) + +class Kodi_Monitor(xbmc.Monitor): + def __init__(self, *args, **kwargs): + xbmc.Monitor.__init__(self) + + def onDatabaseUpdated(self, database): + pass + + #this library monitor is used to detect a watchedstate change by the user through the library + def onNotification (self,sender,method,data): + if method == "VideoLibrary.OnUpdate": + + #check windowprop if the sync is busy to prevent any false updates + if WINDOW.getProperty("librarysync") != "busy": + + jsondata = json.loads(data) + if jsondata != None: + playcount = None + playcount = jsondata.get("playcount") + item = jsondata.get("item").get("id") + + if playcount != None: + librarySync.updatePlayCountFromKodi(item, playcount) + diff --git a/resources/lib/LibrarySync.py b/resources/lib/LibrarySync.py new file mode 100644 index 00000000..5bc2587a --- /dev/null +++ b/resources/lib/LibrarySync.py @@ -0,0 +1,270 @@ +################################################################################################# +# LibrarySync +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon +import xbmcvfs +import json +import threading +import urllib +from datetime import datetime, timedelta, time +import urllib2 +import os +from xml.etree.ElementTree import Element, SubElement, Comment, tostring +from xml.etree import ElementTree +from xml.dom import minidom +import xml.etree.cElementTree as ET + +from API import API +import Utils as utils +from DownloadUtils import DownloadUtils +downloadUtils = DownloadUtils() + +addon = xbmcaddon.Addon(id='plugin.video.mb3sync') +addondir = xbmc.translatePath( addon.getAddonInfo('profile') ) +dataPath = os.path.join(addondir,"library") +movieLibrary = os.path.join(dataPath,'movies') +tvLibrary = os.path.join(dataPath,'tvshows') + +WINDOW = xbmcgui.Window( 10000 ) +port = addon.getSetting('port') +host = addon.getSetting('ipaddress') +server = host + ":" + port +userid = downloadUtils.getUserId() + + +class LibrarySync(): + + def syncDatabase(self): + + WINDOW.setProperty("librarysync", "busy") + updateNeeded = False + + allMovies = list() + for item in self.getMovies(True): + if not item.get('IsFolder'): + kodiItem = self.getKodiMovie(item["Id"]) + allMovies.append(item["Id"]) + if kodiItem == None: + self.addMovieToKodiLibrary(item) + updateNeeded = True + else: + self.updateMovieToKodiLibrary(item, kodiItem) + + cleanNeeded = False + # process deletes + allLocaldirs, filesMovies = xbmcvfs.listdir(movieLibrary) + allMB3Movies = set(allMovies) + for dir in allLocaldirs: + if not dir in allMB3Movies: + self.deleteMovieFromKodiLibrary(dir) + cleanneeded = True + + if cleanNeeded: + xbmc.executebuiltin("CleanLibrary(video)") + + if updateNeeded: + xbmc.executebuiltin("UpdateLibrary(video)") + + WINDOW.clearProperty("librarysync") + + def updatePlayCounts(self): + #update all playcounts from MB3 to Kodi library + + WINDOW.setProperty("librarysync", "busy") + + for item in self.getMovies(False): + if not item.get('IsFolder'): + kodiItem = self.getKodiMovie(item["Id"]) + userData=API().getUserData(item) + timeInfo = API().getTimeInfo(item) + if kodiItem != None: + if kodiItem['playcount'] != int(userData.get("PlayCount")): + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.SetMovieDetails", "params": { "movieid": %i, "playcount": %i}, "id": 1 }' %(kodiItem['movieid'], int(userData.get("PlayCount")))) + if kodiItem['playcount'] != int(userData.get("PlayCount")): + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.SetMovieDetails", "params": { "movieid": %i, "playcount": %i}, "id": 1 }' %(kodiItem['movieid'], int(userData.get("PlayCount")))) + + + WINDOW.clearProperty("librarysync") + + def getMovies(self, fullinfo = False): + result = None + if fullinfo: + url = server + '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=Path,Genres,Studios,CumulativeRunTimeTicks,Metascore,AirTime,DateCreated,MediaStreams,People,Overview&Recursive=true&SortOrder=Ascending&IncludeItemTypes=Movie&format=json&ImageTypeLimit=1' + else: + url = server + '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=CumulativeRunTimeTicks&Recursive=true&SortOrder=Ascending&IncludeItemTypes=Movie&format=json&ImageTypeLimit=1' + + jsonData = downloadUtils.downloadUrl(url, suppress=True, popup=0) + if jsonData != None: + result = json.loads(jsonData) + if(result.has_key('Items')): + result = result['Items'] + + return result + + def updatePlayCountFromKodi(self, id, playcount=0): + #when user marks item watched from kodi interface update this to MB3 + json_response = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetMovieDetails", "params": { "movieid": ' + str(id) + ', "properties" : ["playcount", "file"] }, "id": "1"}') + if json_response != None: + jsonobject = json.loads(json_response.decode('utf-8','replace')) + movie = None + if(jsonobject.has_key('result')): + result = jsonobject['result'] + if(result.has_key('moviedetails')): + moviedetails = result['moviedetails'] + filename = moviedetails.get("file").rpartition('\\')[2] + mb3Id = filename.replace(".strm","") + + watchedurl = 'http://' + host + ':' + port + '/mediabrowser/Users/' + userid + '/PlayedItems/' + mb3Id + print "watchedurl -->" + watchedurl + if playcount != 0: + downloadUtils.downloadUrl(watchedurl, postBody="", type="POST") + else: + downloadUtils.downloadUrl(watchedurl, type="DELETE") + + def updateMovieToKodiLibrary( self, MBitem, KodiItem ): + + #TODO: only update the info if something is actually changed + timeInfo = API().getTimeInfo(MBitem) + userData=API().getUserData(MBitem) + people = API().getPeople(MBitem) + mediaStreams=API().getMediaStreams(MBitem) + + thumbPath = downloadUtils.getArtwork(MBitem, "Primary") + + utils.logMsg("Updating item to Kodi Library", MBitem["Id"] + " - " + MBitem["Name"]) + + #update artwork + self.updateArtWork(KodiItem,"poster", downloadUtils.getArtwork(MBitem, "poster")) + self.updateArtWork(KodiItem,"clearlogo", downloadUtils.getArtwork(MBitem, "Logo")) + self.updateArtWork(KodiItem,"banner", downloadUtils.getArtwork(MBitem, "Banner")) + self.updateArtWork(KodiItem,"landscape", downloadUtils.getArtwork(MBitem, "Thumb")) + self.updateArtWork(KodiItem,"discart", downloadUtils.getArtwork(MBitem, "Disc")) + self.updateArtWork(KodiItem,"fanart", downloadUtils.getArtwork(MBitem, "Backdrop")) + + #update duration + duration = (int(timeInfo.get('Duration'))*60) + if KodiItem['runtime'] != duration: + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.SetMovieDetails", "params": { "movieid": %i, "runtime": %s}, "id": 1 }' %(KodiItem['movieid'], duration)) + + #update year + if KodiItem['year'] != MBitem.get("ProductionYear") and MBitem.get("ProductionYear") != None: + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.SetMovieDetails", "params": { "movieid": %i, "year": %s}, "id": 1 }' %(KodiItem['movieid'], MBitem.get("ProductionYear"))) + + #update strm file - TODO: only update strm when path has changed + self.createSTRM(MBitem["Id"]) + + #update nfo file - needed for testing + nfoFile = os.path.join(movieLibrary,MBitem["Id"],MBitem["Id"] + ".nfo") + if not xbmcvfs.exists(nfoFile): + self.createNFO(MBitem) + + #update playcounts + if KodiItem['playcount'] != int(userData.get("PlayCount")): + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.SetMovieDetails", "params": { "movieid": %i, "playcount": %i}, "id": 1 }' %(KodiItem['movieid'], int(userData.get("PlayCount")))) + + + def updateArtWork(self,KodiItem,artWorkName,artworkValue): + if KodiItem['art'].has_key(artWorkName): + if KodiItem['art'][artWorkName] != artworkValue and artworkValue != None: + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.SetMovieDetails", "params": { "movieid": %i, "art": { "%s": "%s" }}, "id": 1 }' %(KodiItem['movieid'], artWorkName, artworkValue)) + + + def createSTRM(self,id): + + itemPath = os.path.join(movieLibrary,id) + if not xbmcvfs.exists(itemPath): + xbmcvfs.mkdir(itemPath) + + strmFile = os.path.join(itemPath,id + ".strm") + text_file = open(strmFile, "w") + + playUrl = "plugin://plugin.video.mb3sync/?id=" + id + '&mode=play' + + text_file.writelines(playUrl) + text_file.close() + + def createNFO(self,item): + timeInfo = API().getTimeInfo(item) + userData=API().getUserData(item) + people = API().getPeople(item) + mediaStreams=API().getMediaStreams(item) + + #todo: change path if type is not movie + itemPath = os.path.join(movieLibrary,item["Id"]) + nfoFile = os.path.join(itemPath,item["Id"] + ".nfo") + + root = Element("movie") + SubElement(root, "id").text = item["Id"] + SubElement(root, "tag").text = item["Id"] + SubElement(root, "thumb").text = downloadUtils.getArtwork(item, "poster") + SubElement(root, "fanart").text = timeInfo.get('Backdrop') + SubElement(root, "title").text = item["Name"].encode('utf-8').decode('utf-8') + SubElement(root, "originaltitle").text = item["Id"] + + SubElement(root, "year").text = str(item.get("ProductionYear")) + SubElement(root, "runtime").text = str(timeInfo.get('Duration')) + + fileinfo = SubElement(root, "fileinfo") + streamdetails = SubElement(fileinfo, "streamdetails") + video = SubElement(streamdetails, "video") + SubElement(video, "duration").text = str(timeInfo.get('totaltime')) + SubElement(video, "aspect").text = timeInfo.get('aspectratio') + SubElement(video, "codec").text = timeInfo.get('videocodec') + SubElement(video, "width").text = str(timeInfo.get('width')) + SubElement(video, "height").text = str(timeInfo.get('height')) + audio = SubElement(streamdetails, "audio") + SubElement(audio, "codec").text = timeInfo.get('audiocodec') + SubElement(audio, "channels").text = timeInfo.get('channels') + + SubElement(root, "plot").text = API().getOverview(item).decode('utf-8') + + art = SubElement(root, "art") + SubElement(art, "poster").text = downloadUtils.getArtwork(item, "poster") + SubElement(art, "fanart").text = downloadUtils.getArtwork(item, "Backdrop") + SubElement(art, "landscape").text = downloadUtils.getArtwork(item, "Thumb") + SubElement(art, "clearlogo").text = downloadUtils.getArtwork(item, "Logo") + SubElement(art, "discart").text = downloadUtils.getArtwork(item, "Disc") + SubElement(art, "banner").text = downloadUtils.getArtwork(item, "Banner") + + ET.ElementTree(root).write(nfoFile, encoding="utf-8", xml_declaration=True) + + def addMovieToKodiLibrary( self, item ): + itemPath = os.path.join(movieLibrary,item["Id"]) + strmFile = os.path.join(itemPath,item["Id"] + ".strm") + + utils.logMsg("Adding item to Kodi Library",item["Id"] + " - " + item["Name"]) + + #create path if not exists + if not xbmcvfs.exists(itemPath): + xbmcvfs.mkdir(itemPath) + + #create nfo file + self.createNFO(item) + + # create strm file + self.createSTRM(item["Id"]) + + def deleteMovieFromKodiLibrary(self, id ): + kodiItem = self.getKodiMovie(id) + utils.logMsg("deleting movie from Kodi library",id) + if kodiItem != None: + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.RemoveMovie", "params": { "movieid": %i}, "id": 1 }' %(kodiItem["movieid"])) + + path = os.path.join(movieLibrary,id) + xbmcvfs.rmdir(path) + + def getKodiMovie(self, id): + json_response = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetMovies", "params": { "filter": {"operator": "contains", "field": "path", "value": "' + id + '"}, "properties" : ["art", "rating", "thumbnail", "runtime", "year", "plot", "playcount", "file"], "sort": { "order": "ascending", "method": "label", "ignorearticle": true } }, "id": "libMovies"}') + jsonobject = json.loads(json_response.decode('utf-8','replace')) + movie = None + + if(jsonobject.has_key('result')): + result = jsonobject['result'] + if(result.has_key('movies')): + movies = result['movies'] + movie = movies[0] + + return movie diff --git a/resources/lib/PlayUtils.py b/resources/lib/PlayUtils.py new file mode 100644 index 00000000..d321756e --- /dev/null +++ b/resources/lib/PlayUtils.py @@ -0,0 +1,170 @@ +################################################################################################# +# utils class +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +from DownloadUtils import DownloadUtils +from ClientInformation import ClientInformation +import urllib +import sys +import os + +#define our global download utils +downloadUtils = DownloadUtils() +clientInfo = ClientInformation() + +########################################################################### +class PlayUtils(): + + def getPlayUrl(self, server, id, result): + + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + # if the path is local and depending on the video quality play we can direct play it do so- + if self.isDirectPlay(result) == True: + xbmc.log("mb3sync getPlayUrl -> Direct Play") + playurl = result.get("Path") + if playurl != None: + #We have a path to play so play it + USER_AGENT = 'QuickTime/7.7.4' + + # If the file it is not a media stub + if (result.get("IsPlaceHolder") != True): + if (result.get("VideoType") == "Dvd"): + playurl = playurl + "/VIDEO_TS/VIDEO_TS.IFO" + elif (result.get("VideoType") == "BluRay"): + playurl = playurl + "/BDMV/index.bdmv" + if addonSettings.getSetting('smbusername') == '': + playurl = playurl.replace("\\\\", "smb://") + else: + playurl = playurl.replace("\\\\", "smb://" + addonSettings.getSetting('smbusername') + ':' + addonSettings.getSetting('smbpassword') + '@') + playurl = playurl.replace("\\", "/") + + if ("apple.com" in playurl): + playurl += '?|User-Agent=%s' % USER_AGENT + if addonSettings.getSetting('playFromStream') == "true": + playurl = 'http://' + server + '/mediabrowser/Videos/' + id + '/stream?static=true' + mediaSources = result.get("MediaSources") + if(mediaSources != None): + if mediaSources[0].get('DefaultAudioStreamIndex') != None: + playurl = playurl + "&AudioStreamIndex=" +str(mediaSources[0].get('DefaultAudioStreamIndex')) + if mediaSources[0].get('DefaultSubtitleStreamIndex') != None: + playurl = playurl + "&SubtitleStreamIndex=" + str(mediaSources[0].get('DefaultAudioStreamIndex')) + + else: + #No path or has a path but not sufficient network so transcode + xbmc.log("mb3sync getPlayUrl -> Transcode") + if result.get("Type") == "Audio": + playurl = 'http://' + server + '/mediabrowser/Audio/' + id + '/stream.mp3' + else: + txt_mac = clientInfo.getMachineId() + playurl = 'http://' + server + '/mediabrowser/Videos/' + id + '/master.m3u8?mediaSourceId=' + id + playurl = playurl + '&videoCodec=h264' + playurl = playurl + '&AudioCodec=aac,ac3' + playurl = playurl + '&deviceId=' + txt_mac + playurl = playurl + '&VideoBitrate=' + str(int(self.getVideoBitRate()) * 1000) + mediaSources = result.get("MediaSources") + if(mediaSources != None): + if mediaSources[0].get('DefaultAudioStreamIndex') != None: + playurl = playurl + "&AudioStreamIndex=" +str(mediaSources[0].get('DefaultAudioStreamIndex')) + if mediaSources[0].get('DefaultSubtitleStreamIndex') != None: + playurl = playurl + "&SubtitleStreamIndex=" + str(mediaSources[0].get('DefaultSubtitleStreamIndex')) + return playurl.encode('utf-8') + + # Works out if we are direct playing or not + def isDirectPlay(self, result): + if (self.fileExists(result) or (result.get("LocationType") == "FileSystem" and self.isNetworkQualitySufficient(result) == True and self.isLocalPath(result) == False)): + return True + else: + return False + + + # Works out if the network quality can play directly or if transcoding is needed + def isNetworkQualitySufficient(self, result): + settingsVideoBitRate = self.getVideoBitRate() + settingsVideoBitRate = int(settingsVideoBitRate) * 1000 + mediaSources = result.get("MediaSources") + if(mediaSources != None): + if mediaSources[0].get('Bitrate') != None: + if settingsVideoBitRate < int(mediaSources[0].get('Bitrate')): + xbmc.log("mb3sync isNetworkQualitySufficient -> FALSE bit rate - settingsVideoBitRate: " + str(settingsVideoBitRate) + " mediasource bitrate: " + str(mediaSources[0].get('Bitrate'))) + return False + else: + xbmc.log("mb3sync isNetworkQualitySufficient -> TRUE bit rate") + return True + + # Any thing else is ok + xbmc.log("mb3sync isNetworkQualitySufficient -> TRUE default") + return True + + + # get the addon video quality + def getVideoBitRate(self): + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + videoQuality = addonSettings.getSetting('videoBitRate') + if (videoQuality == "0"): + return '664' + elif (videoQuality == "1"): + return '996' + elif (videoQuality == "2"): + return '1320' + elif (videoQuality == "3"): + return '2000' + elif (videoQuality == "4"): + return '3200' + elif (videoQuality == "5"): + return '4700' + elif (videoQuality == "6"): + return '6200' + elif (videoQuality == "7"): + return '7700' + elif (videoQuality == "8"): + return '9200' + elif (videoQuality == "9"): + return '10700' + elif (videoQuality == "10"): + return '12200' + elif (videoQuality == "11"): + return '13700' + elif (videoQuality == "12"): + return '15200' + elif (videoQuality == "13"): + return '16700' + elif (videoQuality == "14"): + return '18200' + elif (videoQuality == "15"): + return '20000' + elif (videoQuality == "16"): + return '40000' + elif (videoQuality == "17"): + return '100000' + elif (videoQuality == "18"): + return '1000000' + + def fileExists(self, result): + path=result.get("Path").encode('utf-8') + if os.path.exists(path) == True: + return True + else: + return False + + + # Works out if the network quality can play directly or if transcoding is needed + def isLocalPath(self, result): + path=result.get("Path").encode('utf-8') + playurl = path + if playurl != None: + #We have a path to play so play it + if ":\\" in playurl: + return True + else: + return False + + # default to not local + return False + diff --git a/resources/lib/PlaybackUtils.py b/resources/lib/PlaybackUtils.py new file mode 100644 index 00000000..949fdc7b --- /dev/null +++ b/resources/lib/PlaybackUtils.py @@ -0,0 +1,180 @@ + +import xbmc +import xbmcplugin +import xbmcgui +import xbmcaddon +import urllib +import datetime +import time +import json as json +import inspect +import sys + +from DownloadUtils import DownloadUtils +downloadUtils = DownloadUtils() +from PlayUtils import PlayUtils +from API import API +import Utils as utils + +addon = xbmcaddon.Addon(id='plugin.video.mb3sync') +language = addon.getLocalizedString + +WINDOW = xbmcgui.Window( 10000 ) +port = addon.getSetting('port') +host = addon.getSetting('ipaddress') +server = host + ":" + port +userid = downloadUtils.getUserId() + + +class PlaybackUtils(): + + settings = None + language = None + logLevel = 0 + + + def __init__(self, *args): + pass + + + def PLAY(self, id): + + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + id + "?format=json&ImageTypeLimit=1", suppress=False, popup=1 ) + result = json.loads(jsonData) + + userData = result.get("UserData") + resume_result = 0 + seekTime = 0 + + if userData.get("PlaybackPositionTicks") != 0: + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + seekTime = reasonableTicks / 10000 + displayTime = str(datetime.timedelta(seconds=seekTime)) + display_list = [ language(30106) + ' ' + displayTime, language(30107)] + resumeScreen = xbmcgui.Dialog() + resume_result = resumeScreen.select(language(30105), display_list) + + + playurl = PlayUtils().getPlayUrl(server, id, result) + xbmc.log("Play URL: " + playurl) + thumbPath = downloadUtils.getArtwork(result, "Primary") + listItem = xbmcgui.ListItem(path=playurl, iconImage=thumbPath, thumbnailImage=thumbPath) + + self.setListItemProps(server, id, listItem, result) + + # Can not play virtual items + if (result.get("LocationType") == "Virtual"): + xbmcgui.Dialog().ok(self.language(30128), language(30129)) + + watchedurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id + positionurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayingItems/' + id + deleteurl = 'http://' + server + '/mediabrowser/Items/' + id + + # set the current playing info + WINDOW.setProperty(playurl+"watchedurl", watchedurl) + WINDOW.setProperty(playurl+"positionurl", positionurl) + WINDOW.setProperty(playurl+"deleteurl", "") + WINDOW.setProperty(playurl+"deleteurl", deleteurl) + if resume_result == 0: + WINDOW.setProperty(playurl+"seektime", str(seekTime)) + else: + WINDOW.clearProperty(playurl+"seektime") + + if result.get("Type")=="Episode": + WINDOW.setProperty(playurl+"refresh_id", result.get("SeriesId")) + else: + WINDOW.setProperty(playurl+"refresh_id", id) + + WINDOW.setProperty(playurl+"runtimeticks", str(result.get("RunTimeTicks"))) + WINDOW.setProperty(playurl+"type", result.get("Type")) + WINDOW.setProperty(playurl+"item_id", id) + + if PlayUtils().isDirectPlay(result) == True: + playMethod = "DirectPlay" + else: + playMethod = "Transcode" + + + WINDOW.setProperty(playurl+"playmethod", playMethod) + + mediaSources = result.get("MediaSources") + if(mediaSources != None): + if mediaSources[0].get('DefaultAudioStreamIndex') != None: + WINDOW.setProperty(playurl+"AudioStreamIndex", str(mediaSources[0].get('DefaultAudioStreamIndex'))) + if mediaSources[0].get('DefaultSubtitleStreamIndex') != None: + WINDOW.setProperty(playurl+"SubtitleStreamIndex", str(mediaSources[0].get('DefaultSubtitleStreamIndex'))) + + #this launches the playback + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem) + + + def setArt(self, list,name,path): + if name=='thumb' or name=='fanart_image' or name=='small_poster' or name=='tiny_poster' or name == "medium_landscape" or name=='medium_poster' or name=='small_fanartimage' or name=='medium_fanartimage' or name=='fanart_noindicators': + list.setProperty(name, path) + else: + list.setArt({name:path}) + return list + + def setListItemProps(self, server, id, listItem, result): + # set up item and item info + userid = downloadUtils.getUserId() + thumbID = id + eppNum = -1 + seasonNum = -1 + tvshowTitle = "" + + if(result.get("Type") == "Episode"): + thumbID = result.get("SeriesId") + seasonNum = result.get("ParentIndexNumber") + eppNum = result.get("IndexNumber") + tvshowTitle = result.get("SeriesName") + + self.setArt(listItem,'poster', downloadUtils.getArtwork(result, "Primary")) + self.setArt(listItem,'tvshow.poster', downloadUtils.getArtwork(result, "SeriesPrimary")) + self.setArt(listItem,'clearart', downloadUtils.getArtwork(result, "Art")) + self.setArt(listItem,'tvshow.clearart', downloadUtils.getArtwork(result, "Art")) + self.setArt(listItem,'clearlogo', downloadUtils.getArtwork(result, "Logo")) + self.setArt(listItem,'tvshow.clearlogo', downloadUtils.getArtwork(result, "Logo")) + self.setArt(listItem,'discart', downloadUtils.getArtwork(result, "Disc")) + self.setArt(listItem,'fanart_image', downloadUtils.getArtwork(result, "Backdrop")) + self.setArt(listItem,'landscape', downloadUtils.getArtwork(result, "Thumb")) + + listItem.setProperty('IsPlayable', 'true') + listItem.setProperty('IsFolder', 'false') + + # Process Studios + studio = API().getStudio(result) + listItem.setInfo('video', {'studio' : studio}) + + # play info + playinformation = '' + if PlayUtils().isDirectPlay(result) == True: + playinformation = language(30165) + else: + playinformation = language(30166) + + details = { + 'title' : result.get("Name", "Missing Name") + ' - ' + playinformation, + 'plot' : result.get("Overview") + } + + if(eppNum > -1): + details["episode"] = str(eppNum) + + if(seasonNum > -1): + details["season"] = str(seasonNum) + + if tvshowTitle != None: + details["TVShowTitle"] = tvshowTitle + + listItem.setInfo( "Video", infoLabels=details ) + + people = API().getPeople(result) + + # Process Genres + genre = API().getGenre(result) + + listItem.setInfo('video', {'director' : people.get('Director')}) + listItem.setInfo('video', {'writer' : people.get('Writer')}) + listItem.setInfo('video', {'mpaa': result.get("OfficialRating")}) + listItem.setInfo('video', {'genre': genre}) diff --git a/resources/lib/Player.py b/resources/lib/Player.py new file mode 100644 index 00000000..d3ab534a --- /dev/null +++ b/resources/lib/Player.py @@ -0,0 +1,313 @@ +import xbmcaddon +import xbmcplugin +import xbmc +import xbmcgui +import os +import threading +import json +import KodiMonitor +import Utils as utils +from DownloadUtils import DownloadUtils +from PlayUtils import PlayUtils +from ClientInformation import ClientInformation +from LibrarySync import LibrarySync +librarySync = LibrarySync() + + +# service class for playback monitoring +class Player( xbmc.Player ): + + logLevel = 0 + played_information = {} + downloadUtils = None + settings = None + playStats = {} + + def __init__( self, *args ): + + self.settings = xbmcaddon.Addon(id='plugin.video.mb3sync') + self.downloadUtils = DownloadUtils() + try: + self.logLevel = int(self.settings.getSetting('logLevel')) + except: + pass + self.printDebug("mb3sync Service -> starting playback monitor service") + self.played_information = {} + pass + + def printDebug(self, msg, level = 1): + if(self.logLevel >= level): + if(self.logLevel == 2): + try: + xbmc.log("mb3sync " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg)) + except UnicodeEncodeError: + xbmc.log("mb3sync " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) + else: + try: + xbmc.log("mb3sync " + str(level) + " -> " + str(msg)) + except UnicodeEncodeError: + xbmc.log("mb3sync " + str(level) + " -> " + str(msg.encode('utf-8'))) + + def deleteItem (self, url): + return_value = xbmcgui.Dialog().yesno(__language__(30091),__language__(30092)) + if return_value: + self.printDebug('Deleting via URL: ' + url) + progress = xbmcgui.DialogProgress() + progress.create(__language__(30052), __language__(30053)) + self.downloadUtils.downloadUrl(url, type="DELETE") + progress.close() + xbmc.executebuiltin("Container.Refresh") + return 1 + else: + return 0 + + def hasData(self, data): + if(data == None or len(data) == 0 or data == "None"): + return False + else: + return True + + def stopAll(self): + + if(len(self.played_information) == 0): + return + + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + self.printDebug("mb3sync Service -> played_information : " + str(self.played_information)) + + for item_url in self.played_information: + data = self.played_information.get(item_url) + + if(data != None): + self.printDebug("mb3sync Service -> item_url : " + item_url) + self.printDebug("mb3sync Service -> item_data : " + str(data)) + + deleteurl = data.get("deleteurl") + runtime = data.get("runtime") + currentPosition = data.get("currentPosition") + item_id = data.get("item_id") + refresh_id = data.get("refresh_id") + currentFile = data.get("currentfile") + + if(refresh_id != None): + #todo: trigger update of single item from MB3, for now trigger full playcounts update + librarySync.updatePlayCounts() + + if(currentPosition != None and self.hasData(runtime)): + runtimeTicks = int(runtime) + self.printDebug("mb3sync Service -> runtimeticks:" + str(runtimeTicks)) + percentComplete = (currentPosition * 10000000) / runtimeTicks + markPlayedAt = float(90) / 100 + + self.printDebug("mb3sync Service -> Percent Complete:" + str(percentComplete) + " Mark Played At:" + str(markPlayedAt)) + self.stopPlayback(data) + + if (percentComplete > markPlayedAt): + gotDeleted = 0 + if(deleteurl != None and deleteurl != ""): + self.printDebug("mb3sync Service -> Offering Delete:" + str(deleteurl)) + gotDeleted = self.deleteItem(deleteurl) + + + self.played_information.clear() + + # stop transcoding - todo check we are actually transcoding? + clientInfo = ClientInformation() + txt_mac = clientInfo.getMachineId() + url = ("http://%s:%s/mediabrowser/Videos/ActiveEncodings" % (addonSettings.getSetting('ipaddress'), addonSettings.getSetting('port'))) + url = url + '?DeviceId=' + txt_mac + self.downloadUtils.downloadUrl(url, type="DELETE") + + def stopPlayback(self, data): + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + + item_id = data.get("item_id") + audioindex = data.get("AudioStreamIndex") + subtitleindex = data.get("SubtitleStreamIndex") + playMethod = data.get("playmethod") + currentPosition = data.get("currentPosition") + positionTicks = str(int(currentPosition * 10000000)) + + url = ("http://%s:%s/mediabrowser/Sessions/Playing/Stopped" % (addonSettings.getSetting('ipaddress'), addonSettings.getSetting('port'))) + + url = url + "?itemId=" + item_id + + url = url + "&canSeek=true" + url = url + "&PlayMethod=" + playMethod + url = url + "&QueueableMediaTypes=Video" + url = url + "&MediaSourceId=" + item_id + url = url + "&PositionTicks=" + positionTicks + if(audioindex != None and audioindex!=""): + url = url + "&AudioStreamIndex=" + audioindex + + if(subtitleindex != None and subtitleindex!=""): + url = url + "&SubtitleStreamIndex=" + subtitleindex + + self.downloadUtils.downloadUrl(url, postBody="", type="POST") + + + def reportPlayback(self): + self.printDebug("reportPlayback Called") + + currentFile = xbmc.Player().getPlayingFile() + + #TODO need to change this to use the one in the data map + playTime = xbmc.Player().getTime() + + data = self.played_information.get(currentFile) + + # only report playback if mb3sync has initiated the playback (item_id has value) + if(data != None and data.get("item_id") != None): + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + + item_id = data.get("item_id") + audioindex = data.get("AudioStreamIndex") + subtitleindex = data.get("SubtitleStreamIndex") + playMethod = data.get("playmethod") + paused = data.get("paused") + + url = ("http://%s:%s/mediabrowser/Sessions/Playing/Progress" % (addonSettings.getSetting('ipaddress'), addonSettings.getSetting('port'))) + + url = url + "?itemId=" + item_id + + url = url + "&canSeek=true" + url = url + "&PlayMethod=" + playMethod + url = url + "&QueueableMediaTypes=Video" + url = url + "&MediaSourceId=" + item_id + + url = url + "&PositionTicks=" + str(int(playTime * 10000000)) + + if(audioindex != None and audioindex!=""): + url = url + "&AudioStreamIndex=" + audioindex + + if(subtitleindex != None and subtitleindex!=""): + url = url + "&SubtitleStreamIndex=" + subtitleindex + + if(paused == None): + paused = "false" + url = url + "&IsPaused=" + paused + + self.downloadUtils.downloadUrl(url, postBody="", type="POST") + + def onPlayBackPaused( self ): + currentFile = xbmc.Player().getPlayingFile() + self.printDebug("PLAYBACK_PAUSED : " + currentFile) + if(self.played_information.get(currentFile) != None): + self.played_information[currentFile]["paused"] = "true" + self.reportPlayback() + + def onPlayBackResumed( self ): + currentFile = xbmc.Player().getPlayingFile() + self.printDebug("PLAYBACK_RESUMED : " + currentFile) + if(self.played_information.get(currentFile) != None): + self.played_information[currentFile]["paused"] = "false" + self.reportPlayback() + + def onPlayBackSeek( self, time, seekOffset ): + self.printDebug("PLAYBACK_SEEK") + self.reportPlayback() + + def onPlayBackStarted( self ): + # Will be called when xbmc starts playing a file + WINDOW = xbmcgui.Window( 10000 ) + self.stopAll() + addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') + + if xbmc.Player().isPlaying(): + currentFile = xbmc.Player().getPlayingFile() + self.printDebug("mb3sync Service -> onPlayBackStarted" + currentFile) + + # grab all the info about this item from the stored windows props + # only ever use the win props here, use the data map in all other places + deleteurl = WINDOW.getProperty(currentFile + "deleteurl") + runtime = WINDOW.getProperty(currentFile + "runtimeticks") + item_id = WINDOW.getProperty(currentFile + "item_id") + refresh_id = WINDOW.getProperty(currentFile + "refresh_id") + audioindex = WINDOW.getProperty(currentFile + "AudioStreamIndex") + subtitleindex = WINDOW.getProperty(currentFile + "SubtitleStreamIndex") + playMethod = WINDOW.getProperty(currentFile + "playmethod") + itemType = WINDOW.getProperty(currentFile + "type") + seekTime = WINDOW.getProperty(currentFile + "seektime") + if seekTime != "": + self.seekToPosition(int(seekTime)) + + if(item_id == None or len(item_id) == 0): + return + + url = ("http://%s:%s/mediabrowser/Sessions/Playing" % (addonSettings.getSetting('ipaddress'), addonSettings.getSetting('port'))) + + url = url + "?itemId=" + item_id + + url = url + "&canSeek=true" + url = url + "&PlayMethod=" + playMethod + url = url + "&QueueableMediaTypes=Video" + url = url + "&MediaSourceId=" + item_id + + if(audioindex != None and audioindex!=""): + url = url + "&AudioStreamIndex=" + audioindex + + if(subtitleindex != None and subtitleindex!=""): + url = url + "&SubtitleStreamIndex=" + subtitleindex + + self.downloadUtils.downloadUrl(url, postBody="", type="POST") + + # save data map for updates and position calls + data = {} + data["deleteurl"] = deleteurl + data["runtime"] = runtime + data["item_id"] = item_id + data["refresh_id"] = refresh_id + data["currentfile"] = currentFile + data["AudioStreamIndex"] = audioindex + data["SubtitleStreamIndex"] = subtitleindex + data["playmethod"] = playMethod + data["Type"] = itemType + self.played_information[currentFile] = data + + self.printDebug("mb3sync Service -> ADDING_FILE : " + currentFile) + self.printDebug("mb3sync Service -> ADDING_FILE : " + str(self.played_information)) + + # log some playback stats + if(itemType != None): + if(self.playStats.get(itemType) != None): + count = self.playStats.get(itemType) + 1 + self.playStats[itemType] = count + else: + self.playStats[itemType] = 1 + + if(playMethod != None): + if(self.playStats.get(playMethod) != None): + count = self.playStats.get(playMethod) + 1 + self.playStats[playMethod] = count + else: + self.playStats[playMethod] = 1 + + # reset in progress position + self.reportPlayback() + + def GetPlayStats(self): + return self.playStats + + def onPlayBackEnded( self ): + # Will be called when xbmc stops playing a file + self.printDebug("mb3sync Service -> onPlayBackEnded") + self.stopAll() + + def onPlayBackStopped( self ): + # Will be called when user stops xbmc playing a file + self.printDebug("mb3sync Service -> onPlayBackStopped") + self.stopAll() + + def seekToPosition(self, seekTo): + + #Jump to resume point + jumpBackSec = 10 + seekToTime = seekTo - jumpBackSec + count = 0 + while xbmc.Player().getTime() < (seekToTime - 5) and count < 11: # only try 10 times + count = count + 1 + xbmc.Player().pause + xbmc.sleep(100) + xbmc.Player().seekTime(seekToTime) + xbmc.sleep(100) + xbmc.Player().play() \ No newline at end of file diff --git a/resources/lib/Utils.py b/resources/lib/Utils.py new file mode 100644 index 00000000..dd6dca90 --- /dev/null +++ b/resources/lib/Utils.py @@ -0,0 +1,162 @@ +################################################################################################# +# utils +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon +import xbmcvfs +import json +import os +import inspect +from xml.etree.ElementTree import Element, SubElement, Comment, tostring +from xml.etree import ElementTree +from xml.dom import minidom +import xml.etree.cElementTree as ET + +from API import API +from PlayUtils import PlayUtils +from DownloadUtils import DownloadUtils +downloadUtils = DownloadUtils() +addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') +language = addonSettings.getLocalizedString + +def logMsg(title, msg, level = 1): + + #todo --> get this from a setting + logLevel = 0 + + if(logLevel >= level): + if(logLevel == 1): + try: + xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg)) + except UnicodeEncodeError: + xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) + else: + try: + xbmc.log(title + " -> " + str(msg)) + except UnicodeEncodeError: + xbmc.log(title + " -> " + str(msg.encode('utf-8'))) + + +def checkKodiSources(): + print "All sources in Kodi -->" + addon = xbmcaddon.Addon(id='plugin.video.mb3sync') + addondir = xbmc.translatePath( addon.getAddonInfo('profile') ) + + dataPath = os.path.join(addondir,"library") + movieLibrary = os.path.join(dataPath,'movies') + tvLibrary = os.path.join(dataPath,'tvshows') + + if not xbmcvfs.exists(dataPath): + xbmcvfs.mkdir(dataPath) + if not xbmcvfs.exists(movieLibrary): + xbmcvfs.mkdir(movieLibrary) + if not xbmcvfs.exists(tvLibrary): + xbmcvfs.mkdir(tvLibrary) + + allKodiSources = list() + + json_response = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Files.GetSources", "params": { "media": "video"}, "id": 1 }') + jsonobject = json.loads(json_response.decode('utf-8','replace')) + + if(jsonobject.has_key('result')): + result = jsonobject['result'] + if(result.has_key('sources')): + for source in result["sources"]: + allKodiSources.append(source["label"]) + + allKodiSources = set(allKodiSources) + + rebootRequired = False + if not "mediabrowser_movies" in allKodiSources: + addKodiSource("mediabrowser_movies",movieLibrary) + rebootRequired = True + if not "mediabrowser_tvshows" in allKodiSources: + addKodiSource("mediabrowser_tvshows",tvLibrary) + rebootRequired = True + + if rebootRequired: + ret = xbmcgui.Dialog().yesno(heading="MediaBrowser Sync service", line1="A restart of Kodi is needed to apply changes. Do you want to reboot now ?") + if ret: + xbmc.executebuiltin("RestartApp") + +def addKodiSource(name, path): + userDataPath = xbmc.translatePath( "special://profile" ) + sourcesFile = os.path.join(userDataPath,'sources.xml') + + print "####parsing sources file #####" + sourcesFile + + tree = ET.ElementTree(file=sourcesFile) + root = tree.getroot() + + videosources = root.find("video") + + #remove any existing entries + allsources = videosources.findall("source") + if allsources != None: + for source in allsources: + if source.find("name").text == name: + videosources.remove(source) + + # add new source + source = SubElement(videosources,'source') + SubElement(source, "name").text = name + SubElement(source, "path").text = path + + tree.write(sourcesFile) + +def checkAuthentication(): + #check authentication + if addonSettings.getSetting('username') != "" and addonSettings.getSetting('ipaddress') != "": + try: + downloadUtils.authenticate() + except Exception, e: + logMsg("MB3 Syncer authentication failed",e) + pass + +def prettifyXml(elem): + rough_string = etree.tostring(elem, "utf-8") + reparsed = minidom.parseString(rough_string) + return reparsed.toprettyxml(indent="\t") + +def doKodiCleanup(): + #remove old testdata and remove missing files + json_response = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetMovies", "params": {"properties" : ["file"], "sort": { "order": "ascending", "method": "label", "ignorearticle": true } }, "id": "libMovies"}') + jsonobject = json.loads(json_response.decode('utf-8','replace')) + if(jsonobject.has_key('result')): + result = jsonobject['result'] + if(result.has_key('movies')): + movies = result['movies'] + for movie in movies: + if (xbmcvfs.exists(movie["file"]) == False) or ("plugin.video.xbmb3c" in movie["file"]): + print "deleting --> " + movie["file"] + xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.RemoveMovie", "params": { "movieid": %i}, "id": 1 }' %(movie["movieid"])) + + +def get_params( paramstring ): + xbmc.log("Parameter string: " + paramstring) + param={} + if len(paramstring)>=2: + params=paramstring + + if params[0] == "?": + cleanedparams=params[1:] + else: + cleanedparams=params + + if (params[len(params)-1]=='/'): + params=params[0:len(params)-2] + + pairsofparams=cleanedparams.split('&') + for i in range(len(pairsofparams)): + splitparams={} + splitparams=pairsofparams[i].split('=') + if (len(splitparams))==2: + param[splitparams[0]]=splitparams[1] + elif (len(splitparams))==3: + param[splitparams[0]]=splitparams[1]+"="+splitparams[2] + xbmc.log("XBMB3C -> Detected parameters: " + str(param)) + return param + + \ No newline at end of file diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py new file mode 100644 index 00000000..b93054b3 --- /dev/null +++ b/resources/lib/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/resources/mb3.png b/resources/mb3.png new file mode 100644 index 00000000..fc266e2e Binary files /dev/null and b/resources/mb3.png differ diff --git a/resources/settings.xml b/resources/settings.xml new file mode 100644 index 00000000..9423ff3b --- /dev/null +++ b/resources/settings.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/service.py b/service.py new file mode 100644 index 00000000..266db3b3 --- /dev/null +++ b/service.py @@ -0,0 +1,101 @@ +import xbmcaddon +import xbmc +import xbmcgui +import os +import threading +import json +from datetime import datetime + + +addonSettings = xbmcaddon.Addon(id='plugin.video.mb3sync') +cwd = addonSettings.getAddonInfo('path') +BASE_RESOURCE_PATH = xbmc.translatePath( os.path.join( cwd, 'resources', 'lib' ) ) +sys.path.append(BASE_RESOURCE_PATH) + +WINDOW = xbmcgui.Window( 10000 ) + +import KodiMonitor +import Utils as utils +from LibrarySync import LibrarySync +from Player import Player +librarySync = LibrarySync() + +class Service(): + + + def __init__(self, *args ): + self.KodiMonitor = KodiMonitor.Kodi_Monitor() + + utils.logMsg("MB3 Sync Service" "starting Monitor",0) + + pass + + + def ServiceEntryPoint(self): + + player = Player() + lastProgressUpdate = datetime.today() + + #perform kodi cleanup (needed while testing, can be removed later if needed) + utils.doKodiCleanup() + + # check kodi library sources + utils.checkKodiSources() + + interval_FullSync = 120 + interval_IncrementalSync = 30 + + cur_seconds_fullsync = 0 + cur_seconds_incrsync = 0 + + while not xbmc.abortRequested: + + xbmc.sleep(1000) + + if xbmc.Player().isPlaying(): + try: + playTime = xbmc.Player().getTime() + currentFile = xbmc.Player().getPlayingFile() + + if(player.played_information.get(currentFile) != None): + player.played_information[currentFile]["currentPossition"] = playTime + + # send update + td = datetime.today() - lastProgressUpdate + secDiff = td.seconds + if(secDiff > 10): + try: + player.reportPlayback() + except Exception, msg: + xbmc.log("MB3 Sync Service -> Exception reporting progress : " + msg) + pass + lastProgressUpdate = datetime.today() + + except Exception, e: + xbmc.log("MB3 Sync Service -> Exception in Playback Monitor Service : " + str(e)) + pass + else: + # background worker for database sync + if WINDOW.getProperty("mb3_authenticated") == "true": + + #full sync + if((interval_FullSync >= cur_seconds_fullsync)): + librarySync.syncDatabase() + cur_seconds_fullsync = interval_FullSync + else: + cur_seconds_fullsync -= 1 + + #incremental sync + if((interval_IncrementalSync >= cur_seconds_incrsync)): + librarySync.updatePlayCounts() + cur_seconds_incrsync = interval_IncrementalSync + else: + cur_seconds_incrsync -= 1 + else: + utils.checkAuthentication() + + utils.logMsg("MB3 Sync Service" "stopping Service",0) + + +#start the service +Service().ServiceEntryPoint()