From 007a6527ae875ddb959fc5c21041663e6140db3f Mon Sep 17 00:00:00 2001 From: quickmic Date: Mon, 15 Mar 2021 08:44:19 +0100 Subject: [PATCH] first stable release of emby-next-gen --- addon.xml | 26 +- context.py | 178 +- context_play.py | 45 - context_transcode.py | 45 - {libraries => core}/__init__.py | 0 core/artwork.py | 208 + core/common.py | 210 + core/kodi.py | 282 + core/listitem.py | 847 +++ core/movies.py | 436 ++ core/music.py | 719 +++ core/musicvideos.py | 246 + core/obj_map.json | 487 ++ core/obj_ops.py | 142 + core/queries_music.py | 75 + core/queries_texture.py | 3 + core/queries_videos.py | 192 + core/tvshows.py | 700 ++ .../dateutil/test => database}/__init__.py | 0 database/database.py | 441 ++ database/emby_db.py | 203 + database/library.py | 832 +++ database/queries.py | 83 + database/sync.py | 437 ++ default.py | 58 - .../urllib3/contrib => dialogs}/__init__.py | 0 dialogs/context.py | 60 + .../lib/dialogs => dialogs}/loginconnect.py | 80 +- .../lib/dialogs => dialogs}/loginmanual.py | 76 +- .../lib/dialogs => dialogs}/serverconnect.py | 72 +- .../lib/dialogs => dialogs}/servermanual.py | 81 +- .../lib/dialogs => dialogs}/usersconnect.py | 49 +- donations.png | Bin 9087 -> 0 bytes {resources/lib => emby}/__init__.py | 0 {libraries/emby => emby}/client.py | 85 +- {resources/lib => emby}/connect.py | 306 +- .../lib/hooks => emby/core}/__init__.py | 0 emby/core/api.py | 421 ++ .../emby => emby}/core/configuration.py | 19 +- .../emby => emby}/core/connection_manager.py | 164 +- {libraries/emby => emby}/core/credentials.py | 39 +- {libraries/emby => emby}/core/exceptions.py | 5 - {libraries/emby => emby}/core/http.py | 126 +- {libraries => emby/core}/websocket.py | 569 +- {libraries/emby => emby}/core/ws_client.py | 31 +- emby/downloader.py | 335 + emby/main.py | 75 + emby/views.py | 887 +++ events.py | 993 +++ .../lib/objects/core => helper}/__init__.py | 0 {resources/lib/helper => helper}/api.py | 111 +- helper/exceptions.py | 5 + .../lib/helper => helper}/loghandler.py | 66 +- helper/setup.py | 104 + {resources/lib/helper => helper}/translate.py | 22 +- helper/utils.py | 703 +++ helper/wrapper.py | 53 + helper/xmls.py | 111 + .../lib/objects/kodi => hooks}/__init__.py | 0 hooks/monitor.py | 850 +++ hooks/player.py | 624 ++ hooks/webservice.py | 124 + libraries/dateutil/LICENSE | 54 - libraries/dateutil/NEWS | 701 -- libraries/dateutil/README.rst | 158 - libraries/dateutil/__init__.py | 8 - libraries/dateutil/_common.py | 43 - libraries/dateutil/easter.py | 89 - libraries/dateutil/parser/__init__.py | 60 - libraries/dateutil/parser/_parser.py | 1578 ----- libraries/dateutil/parser/isoparser.py | 406 -- libraries/dateutil/relativedelta.py | 590 -- libraries/dateutil/rrule.py | 1672 ----- libraries/dateutil/test/_common.py | 275 - .../test/property/test_isoparse_prop.py | 27 - .../test/property/test_parser_prop.py | 22 - libraries/dateutil/test/test_easter.py | 95 - libraries/dateutil/test/test_import_star.py | 33 - libraries/dateutil/test/test_imports.py | 166 - libraries/dateutil/test/test_internals.py | 95 - libraries/dateutil/test/test_isoparser.py | 482 -- libraries/dateutil/test/test_parser.py | 1114 ---- libraries/dateutil/test/test_relativedelta.py | 678 -- libraries/dateutil/test/test_rrule.py | 4842 -------------- libraries/dateutil/test/test_tz.py | 2603 -------- libraries/dateutil/test/test_utils.py | 53 - libraries/dateutil/tz/__init__.py | 17 - libraries/dateutil/tz/_common.py | 415 -- libraries/dateutil/tz/_factories.py | 49 - libraries/dateutil/tz/tz.py | 1785 ------ libraries/dateutil/tz/win.py | 331 - libraries/dateutil/tzwin.py | 2 - libraries/dateutil/utils.py | 71 - libraries/dateutil/zoneinfo/__init__.py | 167 - .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 139130 -> 0 bytes libraries/dateutil/zoneinfo/rebuild.py | 53 - libraries/emby/__init__.py | 126 - libraries/emby/core/__init__.py | 1 - libraries/emby/core/api.py | 472 -- libraries/emby/helpers/__init__.py | 7 - libraries/emby/helpers/utils.py | 15 - libraries/requests/__init__.py | 83 - libraries/requests/adapters.py | 453 -- libraries/requests/api.py | 145 - libraries/requests/auth.py | 223 - libraries/requests/cacert.pem | 5616 ----------------- libraries/requests/certs.py | 25 - libraries/requests/compat.py | 62 - libraries/requests/cookies.py | 487 -- libraries/requests/exceptions.py | 114 - libraries/requests/hooks.py | 34 - libraries/requests/models.py | 851 --- libraries/requests/packages/README.rst | 11 - libraries/requests/packages/__init__.py | 36 - .../requests/packages/chardet/__init__.py | 32 - .../requests/packages/chardet/big5freq.py | 925 --- .../requests/packages/chardet/big5prober.py | 42 - .../requests/packages/chardet/chardetect.py | 80 - .../packages/chardet/chardistribution.py | 231 - .../packages/chardet/charsetgroupprober.py | 106 - .../packages/chardet/charsetprober.py | 62 - .../packages/chardet/codingstatemachine.py | 61 - libraries/requests/packages/chardet/compat.py | 34 - .../requests/packages/chardet/constants.py | 39 - .../requests/packages/chardet/cp949prober.py | 44 - .../requests/packages/chardet/escprober.py | 86 - libraries/requests/packages/chardet/escsm.py | 242 - .../requests/packages/chardet/eucjpprober.py | 90 - .../requests/packages/chardet/euckrfreq.py | 596 -- .../requests/packages/chardet/euckrprober.py | 42 - .../requests/packages/chardet/euctwfreq.py | 428 -- .../requests/packages/chardet/euctwprober.py | 41 - .../requests/packages/chardet/gb2312freq.py | 472 -- .../requests/packages/chardet/gb2312prober.py | 41 - .../requests/packages/chardet/hebrewprober.py | 283 - .../requests/packages/chardet/jisfreq.py | 569 -- libraries/requests/packages/chardet/jpcntx.py | 227 - .../packages/chardet/langbulgarianmodel.py | 229 - .../packages/chardet/langcyrillicmodel.py | 329 - .../packages/chardet/langgreekmodel.py | 225 - .../packages/chardet/langhebrewmodel.py | 201 - .../packages/chardet/langhungarianmodel.py | 225 - .../packages/chardet/langthaimodel.py | 200 - .../requests/packages/chardet/latin1prober.py | 139 - .../packages/chardet/mbcharsetprober.py | 86 - .../packages/chardet/mbcsgroupprober.py | 54 - libraries/requests/packages/chardet/mbcssm.py | 572 -- .../packages/chardet/sbcharsetprober.py | 120 - .../packages/chardet/sbcsgroupprober.py | 69 - .../requests/packages/chardet/sjisprober.py | 91 - .../packages/chardet/universaldetector.py | 170 - .../requests/packages/chardet/utf8prober.py | 76 - .../requests/packages/urllib3/__init__.py | 93 - .../requests/packages/urllib3/_collections.py | 324 - .../requests/packages/urllib3/connection.py | 288 - .../packages/urllib3/connectionpool.py | 818 --- .../packages/urllib3/contrib/appengine.py | 223 - .../packages/urllib3/contrib/ntlmpool.py | 115 - .../packages/urllib3/contrib/pyopenssl.py | 310 - .../requests/packages/urllib3/exceptions.py | 201 - libraries/requests/packages/urllib3/fields.py | 178 - .../requests/packages/urllib3/filepost.py | 94 - .../packages/urllib3/packages/__init__.py | 5 - .../packages/urllib3/packages/ordered_dict.py | 259 - .../requests/packages/urllib3/packages/six.py | 385 -- .../packages/ssl_match_hostname/__init__.py | 13 - .../ssl_match_hostname/_implementation.py | 105 - .../requests/packages/urllib3/poolmanager.py | 281 - .../requests/packages/urllib3/request.py | 151 - .../requests/packages/urllib3/response.py | 514 -- .../packages/urllib3/util/__init__.py | 44 - .../packages/urllib3/util/connection.py | 101 - .../requests/packages/urllib3/util/request.py | 72 - .../packages/urllib3/util/response.py | 74 - .../requests/packages/urllib3/util/retry.py | 286 - .../requests/packages/urllib3/util/ssl_.py | 317 - .../requests/packages/urllib3/util/timeout.py | 242 - .../requests/packages/urllib3/util/url.py | 217 - libraries/requests/sessions.py | 680 -- libraries/requests/status_codes.py | 90 - libraries/requests/structures.py | 104 - libraries/requests/utils.py | 721 --- libraries/six.py | 891 --- resources/__init__.py | 1 - resources/blank.m4v | Bin 0 -> 3753 bytes resources/fanart.jpg | Bin 0 -> 76337 bytes resources/icon.png | Bin 0 -> 1850 bytes resources/kodi_icon.png | Bin 0 -> 16737 bytes .../resource.language.cs_cz/strings.po | 2 +- .../resource.language.de_de/strings.po | 278 +- .../resource.language.en_gb/strings.po | 2 +- .../resource.language.es_es/strings.po | 2 +- .../resource.language.fr_fr/strings.po | 2 +- .../resource.language.it_it/strings.po | 2 +- .../resource.language.nl_nl/strings.po | 2 +- .../resource.language.pl_pl/strings.po | 2 +- .../resource.language.sv_se/strings.po | 2 +- .../resource.language.zh_cn/strings.po | 2 +- resources/lib/client.py | 128 - resources/lib/database/__init__.py | 507 -- resources/lib/database/emby_db.py | 174 - resources/lib/database/queries.py | 188 - resources/lib/dialogs/__init__.py | 5 - resources/lib/dialogs/context.py | 90 - resources/lib/dialogs/resume.py | 58 - resources/lib/downloader.py | 340 - resources/lib/entrypoint/__init__.py | 32 - resources/lib/entrypoint/context.py | 192 - resources/lib/entrypoint/default.py | 1100 ---- resources/lib/entrypoint/service.py | 516 -- resources/lib/helper/__init__.py | 26 - resources/lib/helper/exceptions.py | 10 - resources/lib/helper/playutils.py | 780 --- resources/lib/helper/utils.py | 481 -- resources/lib/helper/wrapper.py | 173 - resources/lib/helper/xmls.py | 169 - resources/lib/hooks/monitor.py | 543 -- resources/lib/hooks/player.py | 601 -- resources/lib/hooks/webservice.py | 432 -- resources/lib/library.py | 1039 --- resources/lib/objects/__init__.py | 5 - resources/lib/objects/play/__init__.py | 0 resources/lib/patch.py | 235 - resources/lib/setup.py | 128 - resources/lib/sync.py | 609 -- resources/lib/views.py | 1064 ---- resources/settings.xml | 68 +- .../script-emby-connect-login-manual.xml | 12 +- .../1080i/script-emby-connect-login.xml | 16 +- .../script-emby-connect-server-manual.xml | 12 +- .../1080i/script-emby-connect-server.xml | 10 +- .../1080i/script-emby-connect-users.xml | 8 +- .../default/1080i/script-emby-context.xml | 2 +- service.py | 273 +- 234 files changed, 12907 insertions(+), 54807 deletions(-) delete mode 100644 context_play.py delete mode 100644 context_transcode.py rename {libraries => core}/__init__.py (100%) create mode 100644 core/artwork.py create mode 100644 core/common.py create mode 100644 core/kodi.py create mode 100644 core/listitem.py create mode 100644 core/movies.py create mode 100644 core/music.py create mode 100644 core/musicvideos.py create mode 100644 core/obj_map.json create mode 100644 core/obj_ops.py create mode 100644 core/queries_music.py create mode 100644 core/queries_texture.py create mode 100644 core/queries_videos.py create mode 100644 core/tvshows.py rename {libraries/dateutil/test => database}/__init__.py (100%) create mode 100644 database/database.py create mode 100644 database/emby_db.py create mode 100644 database/library.py create mode 100644 database/queries.py create mode 100644 database/sync.py delete mode 100644 default.py rename {libraries/requests/packages/urllib3/contrib => dialogs}/__init__.py (100%) create mode 100644 dialogs/context.py rename {resources/lib/dialogs => dialogs}/loginconnect.py (63%) rename {resources/lib/dialogs => dialogs}/loginmanual.py (67%) rename {resources/lib/dialogs => dialogs}/serverconnect.py (72%) rename {resources/lib/dialogs => dialogs}/servermanual.py (65%) rename {resources/lib/dialogs => dialogs}/usersconnect.py (65%) delete mode 100644 donations.png rename {resources/lib => emby}/__init__.py (100%) rename {libraries/emby => emby}/client.py (66%) rename {resources/lib => emby}/connect.py (50%) rename {resources/lib/hooks => emby/core}/__init__.py (100%) create mode 100644 emby/core/api.py rename {libraries/emby => emby}/core/configuration.py (83%) rename {libraries/emby => emby}/core/connection_manager.py (92%) rename {libraries/emby => emby}/core/credentials.py (84%) rename {libraries/emby => emby}/core/exceptions.py (64%) rename {libraries/emby => emby}/core/http.py (70%) rename {libraries => emby/core}/websocket.py (52%) rename {libraries/emby => emby}/core/ws_client.py (76%) create mode 100644 emby/downloader.py create mode 100644 emby/main.py create mode 100644 emby/views.py create mode 100644 events.py rename {resources/lib/objects/core => helper}/__init__.py (100%) rename {resources/lib/helper => helper}/api.py (82%) create mode 100644 helper/exceptions.py rename {resources/lib/helper => helper}/loghandler.py (64%) create mode 100644 helper/setup.py rename {resources/lib/helper => helper}/translate.py (59%) create mode 100644 helper/utils.py create mode 100644 helper/wrapper.py create mode 100644 helper/xmls.py rename {resources/lib/objects/kodi => hooks}/__init__.py (100%) create mode 100644 hooks/monitor.py create mode 100644 hooks/player.py create mode 100644 hooks/webservice.py delete mode 100644 libraries/dateutil/LICENSE delete mode 100644 libraries/dateutil/NEWS delete mode 100644 libraries/dateutil/README.rst delete mode 100644 libraries/dateutil/__init__.py delete mode 100644 libraries/dateutil/_common.py delete mode 100644 libraries/dateutil/easter.py delete mode 100644 libraries/dateutil/parser/__init__.py delete mode 100644 libraries/dateutil/parser/_parser.py delete mode 100644 libraries/dateutil/parser/isoparser.py delete mode 100644 libraries/dateutil/relativedelta.py delete mode 100644 libraries/dateutil/rrule.py delete mode 100644 libraries/dateutil/test/_common.py delete mode 100644 libraries/dateutil/test/property/test_isoparse_prop.py delete mode 100644 libraries/dateutil/test/property/test_parser_prop.py delete mode 100644 libraries/dateutil/test/test_easter.py delete mode 100644 libraries/dateutil/test/test_import_star.py delete mode 100644 libraries/dateutil/test/test_imports.py delete mode 100644 libraries/dateutil/test/test_internals.py delete mode 100644 libraries/dateutil/test/test_isoparser.py delete mode 100644 libraries/dateutil/test/test_parser.py delete mode 100644 libraries/dateutil/test/test_relativedelta.py delete mode 100644 libraries/dateutil/test/test_rrule.py delete mode 100644 libraries/dateutil/test/test_tz.py delete mode 100644 libraries/dateutil/test/test_utils.py delete mode 100644 libraries/dateutil/tz/__init__.py delete mode 100644 libraries/dateutil/tz/_common.py delete mode 100644 libraries/dateutil/tz/_factories.py delete mode 100644 libraries/dateutil/tz/tz.py delete mode 100644 libraries/dateutil/tz/win.py delete mode 100644 libraries/dateutil/tzwin.py delete mode 100644 libraries/dateutil/utils.py delete mode 100644 libraries/dateutil/zoneinfo/__init__.py delete mode 100644 libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz delete mode 100644 libraries/dateutil/zoneinfo/rebuild.py delete mode 100644 libraries/emby/__init__.py delete mode 100644 libraries/emby/core/__init__.py delete mode 100644 libraries/emby/core/api.py delete mode 100644 libraries/emby/helpers/__init__.py delete mode 100644 libraries/emby/helpers/utils.py delete mode 100644 libraries/requests/__init__.py delete mode 100644 libraries/requests/adapters.py delete mode 100644 libraries/requests/api.py delete mode 100644 libraries/requests/auth.py delete mode 100644 libraries/requests/cacert.pem delete mode 100644 libraries/requests/certs.py delete mode 100644 libraries/requests/compat.py delete mode 100644 libraries/requests/cookies.py delete mode 100644 libraries/requests/exceptions.py delete mode 100644 libraries/requests/hooks.py delete mode 100644 libraries/requests/models.py delete mode 100644 libraries/requests/packages/README.rst delete mode 100644 libraries/requests/packages/__init__.py delete mode 100644 libraries/requests/packages/chardet/__init__.py delete mode 100644 libraries/requests/packages/chardet/big5freq.py delete mode 100644 libraries/requests/packages/chardet/big5prober.py delete mode 100644 libraries/requests/packages/chardet/chardetect.py delete mode 100644 libraries/requests/packages/chardet/chardistribution.py delete mode 100644 libraries/requests/packages/chardet/charsetgroupprober.py delete mode 100644 libraries/requests/packages/chardet/charsetprober.py delete mode 100644 libraries/requests/packages/chardet/codingstatemachine.py delete mode 100644 libraries/requests/packages/chardet/compat.py delete mode 100644 libraries/requests/packages/chardet/constants.py delete mode 100644 libraries/requests/packages/chardet/cp949prober.py delete mode 100644 libraries/requests/packages/chardet/escprober.py delete mode 100644 libraries/requests/packages/chardet/escsm.py delete mode 100644 libraries/requests/packages/chardet/eucjpprober.py delete mode 100644 libraries/requests/packages/chardet/euckrfreq.py delete mode 100644 libraries/requests/packages/chardet/euckrprober.py delete mode 100644 libraries/requests/packages/chardet/euctwfreq.py delete mode 100644 libraries/requests/packages/chardet/euctwprober.py delete mode 100644 libraries/requests/packages/chardet/gb2312freq.py delete mode 100644 libraries/requests/packages/chardet/gb2312prober.py delete mode 100644 libraries/requests/packages/chardet/hebrewprober.py delete mode 100644 libraries/requests/packages/chardet/jisfreq.py delete mode 100644 libraries/requests/packages/chardet/jpcntx.py delete mode 100644 libraries/requests/packages/chardet/langbulgarianmodel.py delete mode 100644 libraries/requests/packages/chardet/langcyrillicmodel.py delete mode 100644 libraries/requests/packages/chardet/langgreekmodel.py delete mode 100644 libraries/requests/packages/chardet/langhebrewmodel.py delete mode 100644 libraries/requests/packages/chardet/langhungarianmodel.py delete mode 100644 libraries/requests/packages/chardet/langthaimodel.py delete mode 100644 libraries/requests/packages/chardet/latin1prober.py delete mode 100644 libraries/requests/packages/chardet/mbcharsetprober.py delete mode 100644 libraries/requests/packages/chardet/mbcsgroupprober.py delete mode 100644 libraries/requests/packages/chardet/mbcssm.py delete mode 100644 libraries/requests/packages/chardet/sbcharsetprober.py delete mode 100644 libraries/requests/packages/chardet/sbcsgroupprober.py delete mode 100644 libraries/requests/packages/chardet/sjisprober.py delete mode 100644 libraries/requests/packages/chardet/universaldetector.py delete mode 100644 libraries/requests/packages/chardet/utf8prober.py delete mode 100644 libraries/requests/packages/urllib3/__init__.py delete mode 100644 libraries/requests/packages/urllib3/_collections.py delete mode 100644 libraries/requests/packages/urllib3/connection.py delete mode 100644 libraries/requests/packages/urllib3/connectionpool.py delete mode 100644 libraries/requests/packages/urllib3/contrib/appengine.py delete mode 100644 libraries/requests/packages/urllib3/contrib/ntlmpool.py delete mode 100644 libraries/requests/packages/urllib3/contrib/pyopenssl.py delete mode 100644 libraries/requests/packages/urllib3/exceptions.py delete mode 100644 libraries/requests/packages/urllib3/fields.py delete mode 100644 libraries/requests/packages/urllib3/filepost.py delete mode 100644 libraries/requests/packages/urllib3/packages/__init__.py delete mode 100644 libraries/requests/packages/urllib3/packages/ordered_dict.py delete mode 100644 libraries/requests/packages/urllib3/packages/six.py delete mode 100644 libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py delete mode 100644 libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py delete mode 100644 libraries/requests/packages/urllib3/poolmanager.py delete mode 100644 libraries/requests/packages/urllib3/request.py delete mode 100644 libraries/requests/packages/urllib3/response.py delete mode 100644 libraries/requests/packages/urllib3/util/__init__.py delete mode 100644 libraries/requests/packages/urllib3/util/connection.py delete mode 100644 libraries/requests/packages/urllib3/util/request.py delete mode 100644 libraries/requests/packages/urllib3/util/response.py delete mode 100644 libraries/requests/packages/urllib3/util/retry.py delete mode 100644 libraries/requests/packages/urllib3/util/ssl_.py delete mode 100644 libraries/requests/packages/urllib3/util/timeout.py delete mode 100644 libraries/requests/packages/urllib3/util/url.py delete mode 100644 libraries/requests/sessions.py delete mode 100644 libraries/requests/status_codes.py delete mode 100644 libraries/requests/structures.py delete mode 100644 libraries/requests/utils.py delete mode 100644 libraries/six.py delete mode 100644 resources/__init__.py create mode 100644 resources/blank.m4v create mode 100644 resources/fanart.jpg create mode 100644 resources/icon.png create mode 100644 resources/kodi_icon.png delete mode 100644 resources/lib/client.py delete mode 100644 resources/lib/database/__init__.py delete mode 100644 resources/lib/database/emby_db.py delete mode 100644 resources/lib/database/queries.py delete mode 100644 resources/lib/dialogs/__init__.py delete mode 100644 resources/lib/dialogs/context.py delete mode 100644 resources/lib/dialogs/resume.py delete mode 100644 resources/lib/downloader.py delete mode 100644 resources/lib/entrypoint/__init__.py delete mode 100644 resources/lib/entrypoint/context.py delete mode 100644 resources/lib/entrypoint/default.py delete mode 100644 resources/lib/entrypoint/service.py delete mode 100644 resources/lib/helper/__init__.py delete mode 100644 resources/lib/helper/exceptions.py delete mode 100644 resources/lib/helper/playutils.py delete mode 100644 resources/lib/helper/utils.py delete mode 100644 resources/lib/helper/wrapper.py delete mode 100644 resources/lib/helper/xmls.py delete mode 100644 resources/lib/hooks/monitor.py delete mode 100644 resources/lib/hooks/player.py delete mode 100644 resources/lib/hooks/webservice.py delete mode 100644 resources/lib/library.py delete mode 100644 resources/lib/objects/__init__.py delete mode 100644 resources/lib/objects/play/__init__.py delete mode 100644 resources/lib/patch.py delete mode 100644 resources/lib/setup.py delete mode 100644 resources/lib/sync.py delete mode 100644 resources/lib/views.py diff --git a/addon.xml b/addon.xml index c25e3b35e..a275436c9 100644 --- a/addon.xml +++ b/addon.xml @@ -1,28 +1,18 @@ - + - - - - + + - + video audio image - - - [!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + !String.IsEmpty(Window(10000).Property(emby_context)) - - - - [[!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + [String.IsEqual(ListItem.DBTYPE,movie) | String.IsEqual(ListItem.DBTYPE,episode)]] + !String.IsEmpty(Window(10000).Property(emby_context_transcode)) - - [!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + !String.IsEmpty(Window(10000).Property(emby_context)) + [!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] @@ -31,13 +21,13 @@ en GNU GENERAL PUBLIC LICENSE. Version 2, June 1991 http://emby.media/community/index.php?/forum/99-kodi/ - http://emby.media/ + https://emby.media/ https://github.com/MediaBrowser/plugin.video.emby Welcome to Emby for Kodi A whole new way to manage and view your media library. The Emby addon for Kodi combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Emby - the most powerful fully open source multi-client media metadata indexer and server. Emby for Kodi is the absolute best way to enjoy the incredible Kodi playback engine combined with the power of Emby's centralized database. Features: Direct integration with the Kodi library for native Kodi speed Instant synchronization with the Emby server Full support for Movie, TV and Music collections Emby Server direct stream and transcoding support - use Kodi when you are away from home! Bienvenido a Emby para Kodi una forma completamente nueva de gestionar y ver su biblioteca multimedia. El complemento Emby para Kodi combina lo mejor de Kodi - navegación ultra suave, interfaces hermosas de usuario y la reproducción de cualquier archivo bajo el sol, y Emby - el indexador y servidor de metadatos multimedia multicliente de código abierto más potente. Emby para Kodi es la mejor manera de disfrutar del increíble motor de reproducción de Kodi combinado con el poder de la base de datos centralizada de Emby. Características: Integración directa con la biblioteca de Kodi para una velocidad nativa de sincronización instantánea con el soporte completo del Servidor Emby para colecciones de películas, programas de TV y música. Emby Server soporta transcodificación y transmisión en directo - ¡Usa Kodi cuando estés fuera de casa! - icon.png - fanart.jpg + resources/icon.png + resources/fanart.jpg diff --git a/context.py b/context.py index 3a89a817c..8b44bb6d1 100644 --- a/context.py +++ b/context.py @@ -1,45 +1,165 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - +import json import logging -import os -import sys - import xbmc import xbmcaddon -################################################################################################# +import database.database +import dialogs.context +import emby.main +import helper.translate +import helper.utils +import helper.loghandler + +class Context(): + def __init__(self, play=False, transcode=False, delete=False): + helper.loghandler.reset() + helper.loghandler.config() + self.LOG = logging.getLogger("EMBY.context.Context") + self._selected_option = None + self.Utils = helper.utils.Utils() + self.server_id = None + self.kodi_id = None + self.media = None + self.XML_PATH = (xbmcaddon.Addon('plugin.video.emby-next-gen').getAddonInfo('path'), "default", "1080i") + self.OPTIONS = { + 'Refresh': helper.translate._(30410), + 'Delete': helper.translate._(30409), + 'Addon': helper.translate._(30408), + 'AddFav': helper.translate._(30405), + 'RemoveFav': helper.translate._(30406), + 'Transcode': helper.translate._(30412) + } + +# try: +# self.kodi_id = max(sys.listitem.getVideoInfoTag().getDbId(), 0) or max(sys.listitem.getMusicInfoTag().getDbId(), 0) or None +# self.media = self.get_media_type() +# self.server_id = sys.listitem.getProperty('embyserver') or None +# item_id = sys.listitem.getProperty('embyid') +# except AttributeError: + if xbmc.getInfoLabel('ListItem.Property(embyid)'): + item_id = xbmc.getInfoLabel('ListItem.Property(embyid)') + else: + self.kodi_id = xbmc.getInfoLabel('ListItem.DBID') + self.media = xbmc.getInfoLabel('ListItem.DBTYPE') + item_id = None + + ServerOnline = False + + for i in range(60): + if self.Utils.window('emby_online.bool'): + ServerOnline = True + break + + xbmc.sleep(500) + + if not ServerOnline: + helper.loghandler.reset() + return + + #Load server connection data + self.server = emby.main.Emby(self.server_id).get_client() + emby.main.Emby().set_state(self.Utils.window('emby.server.state.json')) + + for server in self.Utils.window('emby.server.states.json') or []: + emby.main.Emby(server).set_state(self.Utils.window('emby.server.%s.state.json' % server)) + + if item_id: + self.item = self.server['api'].get_item(item_id) + else: + self.item = self.get_item_id() + + if self.item: + if delete: + self.delete_item() + + elif self.select_menu(): + self.action_menu() + + #Get media type based on sys.listitem. If unfilled, base on visible window +# def get_media_type(self): +# media = sys.listitem.getVideoInfoTag().getMediaType() or sys.listitem.getMusicInfoTag().getMediaType() + +# if not media: +# if xbmc.getCondVisibility('Container.Content(albums)'): +# media = "album" +# elif xbmc.getCondVisibility('Container.Content(artists)'): +# media = "artist" +# elif xbmc.getCondVisibility('Container.Content(songs)'): +# media = "song" +# elif xbmc.getCondVisibility('Container.Content(pictures)'): +# media = "picture" +# else: +# self.LOG.info("media is unknown") + +# return media.decode('utf-8') + + #Get synced item from embydb + def get_item_id(self): + item = database.database.get_item(self.kodi_id, self.media) + + if not item: + return {} + + return { + 'Id': item[0], + 'UserData': json.loads(item[4]) if item[4] else {}, + 'Type': item[3] + } + + #Display the select dialog. + #Favorites, Refresh, Delete (opt), Settings. + def select_menu(self): + options = [] + + if self.item['Type'] not in 'Season': + if self.item['UserData'].get('IsFavorite'): + options.append(self.OPTIONS['RemoveFav']) + else: + options.append(self.OPTIONS['AddFav']) + + options.append(self.OPTIONS['Refresh']) + + if self.Utils.settings('enableContextDelete.bool'): + options.append(self.OPTIONS['Delete']) + + options.append(self.OPTIONS['Addon']) + context_menu = dialogs.context.ContextMenu("script-emby-context.xml", *self.XML_PATH) + context_menu.set_options(options) + context_menu.doModal() -__addon__ = xbmcaddon.Addon(id='plugin.video.emby') -__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') -__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8') -__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') -__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') + if context_menu.is_selected(): + self._selected_option = context_menu.get_selected() -sys.path.insert(0, __cache__) -sys.path.insert(0, __pcache__) -sys.path.insert(0, __libraries__) -sys.path.append(__base__) + return self._selected_option -################################################################################################# + def action_menu(self): + selected = self.Utils.StringDecode(self._selected_option) -from entrypoint import Context + if selected == self.OPTIONS['Refresh']: + self.server['api'].refresh_item(self.item['Id']) -################################################################################################# + elif selected == self.OPTIONS['AddFav']: + self.server['api'].favorite(self.item['Id'], True) -LOG = logging.getLogger("EMBY.context") + elif selected == self.OPTIONS['RemoveFav']: + self.server['api'].favorite(self.item['Id'], False) -################################################################################################# + elif selected == self.OPTIONS['Addon']: + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby-next-gen)') + elif selected == self.OPTIONS['Delete']: + self.delete_item() -if __name__ == "__main__": + def delete_item(self): + delete = True - LOG.debug("--->[ context ]") + if not self.Utils.settings('skipContextMenu.bool'): + if not self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33015)): + delete = False - try: - Context() - except Exception as error: - LOG.exception(error) + if delete: + self.server['api'].delete_item(self.item['Id']) + self.Utils.event("LibraryChanged", {'ItemsRemoved': [self.item['Id']], 'ItemsVerify': [self.item['Id']], 'ItemsUpdated': [], 'ItemsAdded': []}) - LOG.info("---<[ context ]") +if __name__ == "__main__": + Context() diff --git a/context_play.py b/context_play.py deleted file mode 100644 index f92bfef75..000000000 --- a/context_play.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import os -import sys - -import xbmc -import xbmcaddon - -################################################################################################# - -__addon__ = xbmcaddon.Addon(id='plugin.video.emby') -__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') -__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8') -__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') -__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') - -sys.path.insert(0, __cache__) -sys.path.insert(0, __pcache__) -sys.path.insert(0, __libraries__) -sys.path.append(__base__) - -################################################################################################# - -from entrypoint import Context - -################################################################################################# - -LOG = logging.getLogger("EMBY.context") - -################################################################################################# - - -if __name__ == "__main__": - - LOG.debug("--->[ context ]") - - try: - Context(play=True) - except Exception as error: - LOG.exception(error) - - LOG.info("---<[ context ]") diff --git a/context_transcode.py b/context_transcode.py deleted file mode 100644 index 81ae54542..000000000 --- a/context_transcode.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import os -import sys - -import xbmc -import xbmcaddon - -################################################################################################# - -__addon__ = xbmcaddon.Addon(id='plugin.video.emby') -__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') -__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8') -__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') -__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') - -sys.path.insert(0, __cache__) -sys.path.insert(0, __pcache__) -sys.path.insert(0, __libraries__) -sys.path.append(__base__) - -################################################################################################# - -from entrypoint import Context - -################################################################################################# - -LOG = logging.getLogger("EMBY.context") - -################################################################################################# - - -if __name__ == "__main__": - - LOG.debug("--->[ context ]") - - try: - Context(transcode=True) - except Exception as error: - LOG.exception(error) - - LOG.info("---<[ context ]") diff --git a/libraries/__init__.py b/core/__init__.py similarity index 100% rename from libraries/__init__.py rename to core/__init__.py diff --git a/core/artwork.py b/core/artwork.py new file mode 100644 index 000000000..e19e37eb2 --- /dev/null +++ b/core/artwork.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +import logging +import os + +try: + from urllib import urlencode +except: + from urllib.parse import urlencode + +import threading +import requests +import xbmcgui +import xbmcvfs + +import helper.translate +import database.database +import emby.main +from . import queries_videos +from . import queries_music +from . import queries_texture + +class Artwork(): + def __init__(self, cursor, Utils): + if cursor: + cursor.execute("PRAGMA database_list;") + self.is_music = 'MyMusic' in cursor.fetchall()[0][2] + + self.LOG = logging.getLogger("EMBY.core.artwork.Artwork") + self.Utils = Utils + self.cursor = cursor + self.threads = [] + self.server = emby.main.Emby() + self.CacheAllEntriesThread = None + + #Update artwork in the video database. + #Only cache artwork if it changed for the main backdrop, poster. + #Delete current entry before updating with the new one. + #Cache fanart and poster in Kodi texture cache. + def update(self, image_url, kodi_id, media, image): + if image == 'poster' and media in ('song', 'artist', 'album'): + return + + try: + self.cursor.execute(queries_videos.get_art, (kodi_id, media, image,)) + url = self.cursor.fetchone()[0] + except TypeError: + self.LOG.debug("ADD to kodi_id %s art: %s", kodi_id, image_url) + self.cursor.execute(queries_videos.add_art, (kodi_id, media, image, image_url)) + else: + if url != image_url: + self.delete_cache(url) + + if not image_url: + return + + self.LOG.info("UPDATE to kodi_id %s art: %s", kodi_id, image_url) + self.cursor.execute(queries_videos.update_art, (image_url, kodi_id, media, image)) + + #Add all artworks + def add(self, artwork, *args): + KODI = { + 'Primary': ['thumb', 'poster'], + 'Banner': "banner", + 'Logo': "clearlogo", + 'Art': "clearart", + 'Thumb': "landscape", + 'Disc': "discart", + 'Backdrop': "fanart" + } + + for art in KODI: + if art == 'Backdrop': + num_backdrops = len(artwork['Backdrop']) + self.cursor.execute(queries_videos.get_backdrops, args + ("fanart%",)) + + if len(self.cursor.fetchall()) > num_backdrops: + self.cursor.execute(queries_videos.delete_backdrops, args + ("fanart_",)) + + self.update(*(artwork['Backdrop'][0] if num_backdrops else "",) + args + ("fanart",)) + + for index, backdrop in enumerate(artwork['Backdrop'][1:]): + self.update(*(backdrop,) + args + ("%s%s" % ("fanart", index + 1),)) + elif art == 'Primary': + for kodi_image in KODI['Primary']: + self.update(*(artwork['Primary'],) + args + (kodi_image,)) + else: + self.update(*(artwork[art],) + args + (KODI[art],)) + + #Delete artwork from kodi database and remove cache for backdrop/posters + def delete(self, *args): + self.cursor.execute(queries_videos.get_art_url, args) + + for row in self.cursor.fetchall(): + self.delete_cache(row[0]) + + #Delete cached artwork + def delete_cache(self, url): + with database.database.Database('texture') as texturedb: + cursor = texturedb.cursor + + try: + cursor.execute(queries_texture.get_cache, (url,)) + cached = cursor.fetchone()[0] + except TypeError: + self.LOG.debug("Could not find cached url: %s", url) + else: + thumbnails = self.Utils.translatePath("special://thumbnails/%s" % cached) + xbmcvfs.delete(thumbnails) + cursor.execute(queries_texture.delete_cache, (url,)) + + if self.is_music: + self.cursor.execute(queries_music.delete_artwork, (url,)) + else: + self.cursor.execute(queries_videos.delete_artwork, (url,)) + + self.LOG.info("DELETE cached %s", cached) + + #This method will sync all Kodi artwork to textures13.dband cache them locally. This takes diskspace! + def cache_textures(self): + if not self.Utils.set_web_server(): + return + + self.LOG.info("<[ cache textures ]") + + if self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33044)): + self.delete_all_cache() + + self._cache_all_video_entries() + self._cache_all_music_entries() + + #Remove all existing textures from the thumbnails folder + def delete_all_cache(self): + self.LOG.info("[ delete all thumbnails ]") + cache = self.Utils.translatePath('special://thumbnails/') + + if xbmcvfs.exists(cache): + dirs, ignored = xbmcvfs.listdir(cache) + + for directory in dirs: + ignored, files = xbmcvfs.listdir(os.path.join(cache, directory)) + + for Filename in files: + cached = os.path.join(cache, directory, Filename) + xbmcvfs.delete(cached) + self.LOG.debug("DELETE cached %s", cached) + + with database.database.Database('texture') as kodidb: + kodidb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + + for table in kodidb.cursor.fetchall(): + name = table[0] + + if name != 'version': + kodidb.cursor.execute("DELETE FROM " + name) + + #Cache all artwork from video db. Don't include actors + def _cache_all_video_entries(self): + with database.database.Database('video') as kodidb: + kodidb.cursor.execute(queries_videos.get_artwork) + urls = kodidb.cursor.fetchall() + + self.CacheAllEntriesThread = CacheAllEntries(urls, "video", self.Utils) + self.CacheAllEntriesThread.start() + + #Cache all artwork from music db + def _cache_all_music_entries(self): + with database.database.Database('music') as kodidb: + kodidb.cursor.execute(queries_music.get_artwork) + urls = kodidb.cursor.fetchall() + + self.CacheAllEntriesThread = CacheAllEntries(urls, "music", self.Utils) + self.CacheAllEntriesThread.start() + +class CacheAllEntries(threading.Thread): + def __init__(self, urls, Label, Utils): + self.Utils = Utils + self.urls = urls + self.Label = Label + self.progress_updates = xbmcgui.DialogProgressBG() + self.progress_updates.create(helper.translate._('addon_name'), helper.translate._(33045)) + threading.Thread.__init__(self) + + #Cache all entries + def run(self): + webServerPort = self.Utils.window('webServerPort') + webServerUser = self.Utils.window('webServerUser') + webServerPass = self.Utils.window('webServerPass') + total = len(self.urls) + + for index, url in enumerate(self.urls): + if self.Utils.window('emby_should_stop.bool'): + break + + Value = int((float(float(index)) / float(total)) * 100) + self.progress_updates.update(Value, message="%s: %s" % (helper.translate._(33045), self.Label + ": " + str(index))) + + if url[0]: + url = urlencode({'blahblahblah': url[0]}) + url = url[13:] + url = urlencode({'blahblahblah': url}) + url = url[13:] + + try: + requests.head("http://127.0.0.1:%s/image/image://%s" % (webServerPort, url), auth=(webServerUser, webServerPass)) + except: + break + + self.progress_updates.close() diff --git a/core/common.py b/core/common.py new file mode 100644 index 000000000..9e82a2c39 --- /dev/null +++ b/core/common.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +import logging +import database.queries + +class Common(): + def __init__(self, emby_db, objects, Utils, direct_path, Server): + self.LOG = logging.getLogger("EMBY.core.common.Common") + self.Utils = Utils + self.emby_db = emby_db + self.objects = objects + self.direct_path = direct_path + self.server = Server + + #Add streamdata + def Streamdata_add(self, obj, Update): + if Update: + self.emby_db.remove_item_streaminfos(obj['Id']) + + if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + if len(obj['Item']['MediaSources']) >= 2: + Temp = obj['Item']['MediaSources'][1] + obj['Item']['MediaSources'][1] = obj['Item']['MediaSources'][0] + obj['Item']['MediaSources'][0] = Temp + + CountMediaSources = 0 + + for DataSource in obj['Item']['MediaSources']: + DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') + DataSource['emby_id'] = obj['Item']['Id'] + DataSource['MediaIndex'] = CountMediaSources + DataSource['Formats'] = "" + DataSource['RequiredHttpHeaders'] = "" + CountMediaStreamAudio = 0 + CountMediaStreamVideo = 0 + CountMediaSubtitle = 0 + CountStreamSources = 0 + DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') + self.emby_db.add_mediasource(*self.Utils.values(DataSource, database.queries.add_mediasource_obj)) + + for DataStream in DataSource['MediaStreams']: + DataStream['emby_id'] = obj['Item']['Id'] + DataStream['MediaIndex'] = CountMediaSources + DataStream['StreamIndex'] = CountStreamSources + + if DataStream['Type'] == "Video": + DataStream = self.objects.MapMissingData(DataStream, 'VideoStreams') + DataStream['VideoIndex'] = CountMediaStreamVideo + self.emby_db.add_videostreams(*self.Utils.values(DataStream, database.queries.add_videostreams_obj)) + CountMediaStreamVideo += 1 + elif DataStream['Type'] == "Audio": + DataStream = self.objects.MapMissingData(DataStream, 'AudioStreams') + DataStream['AudioIndex'] = CountMediaStreamAudio + self.emby_db.add_audiostreams(*self.Utils.values(DataStream, database.queries.add_audiostreams_obj)) + CountMediaStreamAudio += 1 + elif DataStream['Type'] == "Subtitle": + DataStream = self.objects.MapMissingData(DataStream, 'Subtitles') + DataStream['SubtitleIndex'] = CountMediaSubtitle + self.emby_db.add_subtitles(*self.Utils.values(DataStream, database.queries.add_subtitles_obj)) + CountMediaSubtitle += 1 + + CountStreamSources += 1 + + CountMediaSources += 1 + + return obj + + def get_path_filename(self, obj, MediaID): + #Native Kodi plugins starts with plugin:// -> If native Kodi plugin, drop the link directly in Kodi DB. Emby server cannot play Kodi-Plugins + KodiPluginPath = False + + if obj['Path'].startswith("plugin://"): + KodiPluginPath = True + + if self.direct_path or KodiPluginPath: + if KodiPluginPath: + obj['Filename'] = obj['Path'] + else: + obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] + + obj['Path'] = self.Utils.StringDecode(obj['Path']) + obj['Filename'] = self.Utils.StringDecode(obj['Filename']) + + if not self.Utils.validate(obj['Path']): + raise Exception("Failed to validate path. User stopped.") + + obj['Path'] = obj['Path'].replace(obj['Filename'], "") + + if MediaID == "audio": + return obj + + #Detect Multipart videos + if 'PartCount' in obj['Item']: + if (obj['Item']['PartCount']) >= 2: + AdditionalParts = self.server['api'].get_additional_parts(obj['Id']) + obj['Filename'] = obj['Path'] + obj['Filename'] + obj['StackTimes'] = str(obj['Runtime']) + + for AdditionalItem in AdditionalParts['Items']: + AdditionalItem = self.objects.MapMissingData(AdditionalItem, 'MediaSources') + Path = self.Utils.StringDecode(AdditionalItem['Path']) + obj['Filename'] = obj['Filename'] + " , " + Path + RunTimePart = round(float((AdditionalItem['RunTimeTicks'] or 0) / 10000000.0), 6) + obj['Runtime'] = obj['Runtime'] + RunTimePart + obj['StackTimes'] = str(obj['StackTimes']) + "," + str(obj['Runtime']) + + obj['Filename'] = "stack://" + obj['Filename'] + else: + Filename = self.Utils.PathToFilenameReplaceSpecialCharecters(obj['Path']) + + if MediaID == "tvshows": + obj['Path'] = "http://127.0.0.1:57578/tvshows/%s/" % obj['SeriesId'] + + try: + obj['Filename'] = "%s-%s-%s-stream-%s" % (obj['Id'], obj['Item']['MediaSources'][0]['Id'], obj['Item']['MediaSources'][0]['MediaStreams'][0]['BitRate'], Filename) + except: + obj['Filename'] = "%s-%s-stream-%s" % (obj['Id'], obj['Item']['MediaSources'][0]['Id'], Filename) + self.LOG.warning("No video bitrate available %s", self.Utils.StringMod(obj['Item']['Path'])) + elif MediaID == "movies": + obj['Path'] = "http://127.0.0.1:57578/movies/%s/" % obj['LibraryId'] + + try: + obj['Filename'] = "%s-%s-%s-stream-%s" % (obj['Id'], obj['MediaSourceID'], obj['Item']['MediaSources'][0]['MediaStreams'][0]['BitRate'], Filename) + except: + obj['Filename'] = "%s-%s-stream-%s" % (obj['Id'], obj['MediaSourceID'], Filename) + self.LOG.warning("No video bitrate available %s", self.Utils.StringMod(obj['Item']['Path'])) + elif MediaID == "musicvideos": + obj['Path'] = "http://127.0.0.1:57578/musicvideos/%s/" % obj['LibraryId'] + + try: + obj['Filename'] = "%s-%s-%s-stream-%s" % (obj['Id'], obj['PresentationKey'], obj['Streams']['video'][0]['BitRate'], Filename) + except: + obj['Filename'] = "%s-%s-stream-%s" % (obj['Id'], obj['PresentationKey'], Filename) + self.LOG.warning("No video bitrate available %s", self.Utils.StringMod(obj['Item']['Path'])) + elif MediaID == "audio": + obj['Path'] = "http://127.0.0.1:57578/audio/%s/" % obj['Id'] + obj['Filename'] = "%s-stream-%s" % (obj['Id'], Filename) + return obj + + #Detect Multipart videos + if 'PartCount' in obj['Item']: + if (obj['Item']['PartCount']) >= 2: + AdditionalParts = self.server['api'].get_additional_parts(obj['Id']) + obj['Filename'] = obj['Path'] + obj['Filename'] + obj['StackTimes'] = str(obj['Runtime']) + + for AdditionalItem in AdditionalParts['Items']: + AdditionalItem = self.objects.MapMissingData(AdditionalItem, 'MediaSources') + Filename = self.Utils.PathToFilenameReplaceSpecialCharecters(AdditionalItem['Path']) + + try: + obj['Filename'] = obj['Filename'] + " , " + obj['Path'] + "%s--%s-stream-%s" % (AdditionalItem['Id'], AdditionalItem['MediaSources'][0]['MediaStreams'][0]['BitRate'], Filename) + except: + obj['Filename'] = obj['Filename'] + " , " + obj['Path'] + "%s--stream-%s" % (AdditionalItem['Id'], Filename) + + RunTimePart = round(float((AdditionalItem['RunTimeTicks'] or 0) / 10000000.0), 6) + obj['Runtime'] = obj['Runtime'] + RunTimePart + obj['StackTimes'] = str(obj['StackTimes']) + "," + str(obj['Runtime']) + + obj['Filename'] = "stack://" + obj['Filename'] + + return obj + + def library_check(self, e_item, item, library): + if library is None: + if e_item: + view_id = e_item[6] + view_name = self.emby_db.get_view_name(view_id) + else: + ancestors = self.server['api'].get_ancestors(item['Id']) + + if not ancestors: + if item['Type'] == 'MusicArtist': + try: + views = self.emby_db.get_views_by_media('music')[0] + view_id = views[0] + view_name = views[1] + except: + view_id = None + view_name = None + + else: # Grab the first music library + view_id = None + view_name = None + else: + for ancestor in ancestors: + if ancestor['Type'] == 'CollectionFolder': + view = self.emby_db.get_view_name(ancestor['Id']) + + if not view: + view_id = None + view_name = None + else: + view_id = ancestor['Id'] + view_name = ancestor['Name'] + + break + + sync = database.database.get_sync() + + if not library: + library = {} + + if view_id not in [x.replace('Mixed:', "") for x in sync['Whitelist'] + sync['Libraries']]: + self.LOG.info("Library %s is not synced. Skip update.", view_id) + return False + + library['Id'] = view_id + library['Name'] = view_name + + return library diff --git a/core/kodi.py b/core/kodi.py new file mode 100644 index 000000000..e7ce84031 --- /dev/null +++ b/core/kodi.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +import logging + +import helper.utils +from . import artwork +from . import queries_videos + +class Kodi(): + def __init__(self, cursor): + self.LOG = logging.getLogger("EMBY.core.kodi.Kodi") + self.Utils = helper.utils.Utils() + self.cursor = cursor + self.artwork = artwork.Artwork(cursor, self.Utils) + + def create_entry_path(self): + self.cursor.execute(queries_videos.create_path) + return self.cursor.fetchone()[0] + 1 + + def create_entry_file(self): + self.cursor.execute(queries_videos.create_file) + return self.cursor.fetchone()[0] + 1 + + def create_entry_rating(self): + self.cursor.execute(queries_videos.create_rating) + return self.cursor.fetchone()[0] + 1 + + def create_entry_person(self): + self.cursor.execute(queries_videos.create_person) + return self.cursor.fetchone()[0] + 1 + + def create_entry_genre(self): + self.cursor.execute(queries_videos.create_genre) + return self.cursor.fetchone()[0] + 1 + + def create_entry_studio(self): + self.cursor.execute(queries_videos.create_studio) + return self.cursor.fetchone()[0] + 1 + + def create_entry_bookmark(self): + self.cursor.execute(queries_videos.create_bookmark) + return self.cursor.fetchone()[0] + 1 + + def create_entry_tag(self): + self.cursor.execute(queries_videos.create_tag) + return self.cursor.fetchone()[0] + 1 + + def add_path(self, *args): + path_id = self.get_path(*args) + + if path_id is None: + path_id = self.create_entry_path() + self.cursor.execute(queries_videos.add_path, (path_id,) + args) + + return path_id + + def get_path(self, *args): + try: + self.cursor.execute(queries_videos.get_path, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def update_path(self, *args): + self.cursor.execute(queries_videos.update_path, args) + + def add_link(self, link, person_id, args): + self.cursor.execute(queries_videos.get_update_link.replace("{LinkType}", link), (person_id,) + args) + Temp = self.cursor.fetchone() + + #No primary Key in DB -> INSERT OR REPLACE not working -> check manually + if not Temp: + self.cursor.execute(queries_videos.update_link.replace("{LinkType}", link), (person_id,) + args) + + def remove_path(self, *args): + self.cursor.execute(queries_videos.delete_path, args) + + def add_file(self, filename, path_id): + try: + self.cursor.execute(queries_videos.get_file, (path_id, filename,)) + file_id = self.cursor.fetchone()[0] + except TypeError: + file_id = self.create_entry_file() + self.cursor.execute(queries_videos.add_file, (file_id, path_id, filename)) + + return file_id + + def update_file(self, *args): + self.cursor.execute(queries_videos.update_file, args) + + def remove_file(self, path, *args): + path_id = self.get_path(path) + + if path_id is not None: + self.cursor.execute(queries_videos.delete_file_by_path, (path_id,) + args) + + def get_filename(self, *args): + try: + self.cursor.execute(queries_videos.get_filename, args) + return self.cursor.fetchone()[0] + except TypeError: + return "" + + def add_people(self, people, *args): + def add_thumbnail(person_id, person, person_type): + if person['imageurl']: + art = person_type.lower() + if "writing" in art: + art = "writer" + + self.artwork.update(person['imageurl'], person_id, art, "thumb") + + cast_order = 1 + + for person in people: + if 'Name' not in person: + self.LOG.error("Unable to identify person object") + self.LOG.error(person) + continue + + person_id = self.get_person(person['Name']) + + if person['Type'] == 'Actor': + role = person.get('Role') + self.cursor.execute(queries_videos.update_actor, (person_id,) + args + (role, cast_order,)) + cast_order += 1 + elif person['Type'] == 'Director': + self.add_link('director_link', person_id, args) + elif person['Type'] == 'Writer': + self.add_link('writer_link', person_id, args) + elif person['Type'] == 'Artist': + self.add_link('actor_link', person_id, args) + + add_thumbnail(person_id, person, person['Type']) + + def add_person(self, *args): + person_id = self.create_entry_person() + self.cursor.execute(queries_videos.add_person, (person_id,) + args) + return person_id + + def get_person(self, *args): + try: + self.cursor.execute(queries_videos.get_person, args) + return self.cursor.fetchone()[0] + except TypeError: + return self.add_person(*args) + + #Delete current genres first for clean slate + def add_genres(self, genres, *args): + self.cursor.execute(queries_videos.delete_genres, args) + + for genre in genres: + self.cursor.execute(queries_videos.update_genres, (self.get_genre(genre),) + args) + + def add_genre(self, *args): + genre_id = self.create_entry_genre() + self.cursor.execute(queries_videos.add_genre, (genre_id,) + args) + return genre_id + + def get_genre(self, *args): + try: + self.cursor.execute(queries_videos.get_genre, args) + return self.cursor.fetchone()[0] + except TypeError: + return self.add_genre(*args) + + def add_studios(self, studios, *args): + for studio in studios: + studio_id = self.get_studio(studio) + self.cursor.execute(queries_videos.update_studios, (studio_id,) + args) + + def add_studio(self, *args): + studio_id = self.create_entry_studio() + self.cursor.execute(queries_videos.add_studio, (studio_id,) + args) + return studio_id + + def get_studio(self, *args): + try: + self.cursor.execute(queries_videos.get_studio, args) + return self.cursor.fetchone()[0] + except TypeError: + return self.add_studio(*args) + + #First remove any existing entries + #Then re-add video, audio and subtitles + def add_streams(self, file_id, streams, runtime): + self.cursor.execute(queries_videos.delete_streams, (file_id,)) + + if streams: + for track in streams['video']: + track['FileId'] = file_id + track['Runtime'] = runtime + self.add_stream_video(*self.Utils.values(track, queries_videos.add_stream_video_obj)) + + for track in streams['audio']: + track['FileId'] = file_id + self.add_stream_audio(*self.Utils.values(track, queries_videos.add_stream_audio_obj)) + + for track in streams['subtitle']: + self.add_stream_sub(*self.Utils.values({'language': track, 'FileId': file_id}, queries_videos.add_stream_sub_obj)) + + def add_stream_video(self, *args): + self.cursor.execute(queries_videos.add_stream_video, args) + + def add_stacktimes(self, *args): + self.cursor.execute(queries_videos.add_stacktimes, args) + + def add_stream_audio(self, *args): + self.cursor.execute(queries_videos.add_stream_audio, args) + + def add_stream_sub(self, *args): + self.cursor.execute(queries_videos.add_stream_sub, args) + + #Delete the existing resume point + #Set the watched count + def add_playstate(self, file_id, playcount, date_played, resume, *args): + self.cursor.execute(queries_videos.delete_bookmark, (file_id,)) + self.set_playcount(playcount, date_played, file_id) + + if resume: + bookmark_id = self.create_entry_bookmark() + self.cursor.execute(queries_videos.add_bookmark, (bookmark_id, file_id, resume,) + args) + + def set_playcount(self, *args): + self.cursor.execute(queries_videos.update_playcount, args) + +# def add_settings(self, *args): +# self.cursor.execute(queries_videos.update_settings, args) + +# def get_settings(self, *args): +# self.cursor.execute(queries_videos.get_settings, args) +# return self.cursor.fetchone() + + def add_tags(self, tags, *args): + self.cursor.execute(queries_videos.delete_tags, args) + + for tag in tags: +# tag_id = self.get_tag(tag, *args) + self.get_tag(tag, *args) + + def add_tag(self, *args): + tag_id = self.create_entry_tag() + self.cursor.execute(queries_videos.add_tag, (tag_id,) + args) + return tag_id + + def get_tag(self, tag, *args): + try: + self.cursor.execute(queries_videos.get_tag, (tag,)) + tag_id = self.cursor.fetchone()[0] + except TypeError: + tag_id = self.add_tag(tag) + + self.cursor.execute(queries_videos.update_tag, (tag_id,) + args) + return tag_id + + def remove_tag(self, tag, *args): + try: + self.cursor.execute(queries_videos.get_tag, (tag,)) + tag_id = self.cursor.fetchone()[0] + except TypeError: + return + + self.cursor.execute(queries_videos.delete_tag, (tag_id,) + args) + + def get_rating_id(self, *args): + try: + self.cursor.execute(queries_videos.get_rating, args) + + return self.cursor.fetchone()[0] + except TypeError: + return self.create_entry_rating() + + #Add ratings, rating type and votes + def add_ratings(self, *args): + self.cursor.execute(queries_videos.add_rating, args) + + #Update rating by rating_id + def update_ratings(self, *args): + self.cursor.execute(queries_videos.update_rating, args) + + #Remove all unique ids associated. + def remove_unique_ids(self, *args): + self.cursor.execute(queries_videos.delete_unique_ids, args) diff --git a/core/listitem.py b/core/listitem.py new file mode 100644 index 000000000..23bc5221a --- /dev/null +++ b/core/listitem.py @@ -0,0 +1,847 @@ +# -*- coding: utf-8 -*- +import logging +import xbmcgui + +from helper import api +from . import obj_ops + +class BaseListItem(object): + def __init__(self, obj_type, art_type, art_parent, listitem, item, Utils): + self.LOG = logging.getLogger("EMBY.code.listitem.BaseListItem") + self.li = listitem + self.item = item + self.Utils = Utils + self.objects = obj_ops.Objects(self.Utils) + self.api = api.API(item, self.Utils, item['LI']['Server']) + self.obj = self._get_objects(obj_type) + self.obj['Artwork'] = self._get_artwork(art_type, art_parent) + self.format() + self.set() + + def __getitem__(self, key): + if key in self.obj: + return self.obj[key] + + return None + + def __setitem__(self, key, value): + self.obj[key] = value + + def _get_objects(self, key): + return self.objects.map(self.item, key) + + def _get_artwork(self, key, parent=False): + return self.api.get_all_artwork(self.objects.map(self.item, key), parent) + + #Format object values. Override + def format(self): + pass + + #Set the listitem values based on object. Override + def set(self): + pass + + #Return artwork mapping for object. Override if needed + @classmethod + def art(cls): + return { + 'poster': "Primary", + 'clearart': "Art", + 'clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb", + 'thumb': "Primary", + 'fanart': "Backdrop" + } + + def set_art(self): + artwork = self['Artwork'] + art = self.art() + + for kodi, emby in list(art.items()): + if emby == 'Backdrop': + self._set_art(kodi, artwork[emby][0] if artwork[emby] else " ") + else: + self._set_art(kodi, artwork.get(emby, " ")) + + def _set_art(self, art, path): + self.LOG.debug(" [ art/%s ] %s", art, path) + + if art in ('fanart_image', 'small_poster', 'tiny_poster', 'medium_landscape', 'medium_poster', 'small_fanartimage', 'medium_fanartimage', 'fanart_noindicators', 'tvshow.poster'): + self.li.setProperty(art, path) + else: + self.li.setArt({art: path}) + +class Playlist(BaseListItem): + def __init__(self, *args, **kwargs): + BaseListItem.__init__(self, 'BrowseFolder', 'Artwork', False, *args, **kwargs) + + def set(self): + self.li.setProperty('path', self['Artwork']['Primary']) + self.li.setProperty('IsFolder', 'true') +# self.li.setThumbnailImage(self['Artwork']['Primary']) +# self.li.setIconImage('DefaultFolder.png') + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + self.li.setProperty('IsPlayable', 'false') + self.li.setLabel(self['Title']) + self.li.setContentLookup(False) + +class Channel(BaseListItem): + def __init__(self, *args, **kwargs): + BaseListItem.__init__(self, 'BrowseChannel', 'Artwork', False, *args, **kwargs) + + @staticmethod + def art(): + return { + 'fanart_image': "Backdrop", + 'thumb': "Primary", + 'fanart': "Backdrop" + } + + def format(self): + self['Title'] = "%s - %s" % (self['Title'], self['ProgramName']) + self['Runtime'] = round(float((self['Runtime'] or 0) / 10000000.0), 6) + self['PlayCount'] = self.api.get_playcount(self['Played'], self['PlayCount']) or 0 + self['Overlay'] = 7 if self['Played'] else 6 + self['Artwork']['Primary'] = self['Artwork']['Primary'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + self['Artwork']['Thumb'] = self['Artwork']['Thumb'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + self['Artwork']['Backdrop'] = self['Artwork']['Backdrop'] or ["special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg"] + + def set(self): + metadata = { + 'title': self['Title'], + 'originaltitle': self['Title'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'] + } +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) +## self.set_art() + self.li.setProperty('totaltime', str(self['Runtime'])) + self.li.setProperty('IsPlayable', 'true') + self.li.setProperty('IsFolder', 'false') + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class Photo(BaseListItem): + def __init__(self, *args, **kwargs): + BaseListItem.__init__(self, 'BrowsePhoto', 'Artwork', False, *args, **kwargs) + + def format(self): + self['Overview'] = self.api.get_overview(self['Overview']) + + try: + self['FileDate'] = "%s.%s.%s" % tuple(reversed(self['FileDate'].split('T')[0].split('-'))) + except: + pass + + def set(self): + metadata = { + 'title': self['Title'], + 'picturepath': self['Artwork']['Primary'], + 'date': self['FileDate'], + 'exif:width': str(self.obj.get('Width', 0)), + 'exif:height': str(self.obj.get('Height', 0)), + 'size': self['Size'], + 'exif:cameramake': self['CameraMake'], + 'exif:cameramodel': self['CameraModel'], + 'exif:exposuretime': str(self['ExposureTime']), + 'exif:focallength': str(self['FocalLength']) + } + self.li.setProperty('path', self['Artwork']['Primary']) +# self.li.setThumbnailImage(self['Artwork']['Primary']) + +# if art in ('fanart_image', 'small_poster', 'tiny_poster', +# 'medium_landscape', 'medium_poster', 'small_fanartimage', +# 'medium_fanartimage', 'fanart_noindicators', 'discart', +# 'tvshow.poster'): + +#thumb string - image filename +#poster string - image filename +#banner string - image filename +#fanart string - image filename +#learart string - image filename +#clearlogo string - image filename +#landscape string - image filename +#icon string - image filename + +#listitem.setArt({"thumb":thumb, "fanart": fanart}) + +#Function: setAvailableFanart(images) +#image string (http://www.someurl.com/someimage.png) +#preview [opt] string (http://www.someurl.com/somepreviewimage.png) + +# self.li.setArt({"thumb": self['Artwork']['Primary']}) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + self.li.setProperty('plot', self['Overview']) + self.li.setProperty('IsFolder', 'false') +# self.li.setIconImage('DefaultPicture.png') +# self.li.setProperty('IsPlayable', 'false') + self.li.setLabel(self['Title']) + self.li.setInfo('pictures', metadata) + self.li.setContentLookup(False) + +class Music(BaseListItem): + def __init__(self, *args, **kwargs): + BaseListItem.__init__(self, 'BrowseAudio', 'ArtworkMusic', True, *args, **kwargs) + + @classmethod + def art(cls): + return { + 'clearlogo': "Logo", + 'discart': "Disc", + 'fanart': "Backdrop", + 'fanart_image': "Backdrop", + 'thumb': "Primary" + } + + def format(self): + self['Runtime'] = round(float((self['Runtime'] or 0) / 10000000.0), 6) + self['PlayCount'] = self.api.get_playcount(self['Played'], self['PlayCount']) or 0 + self['Rating'] = self['Rating'] or 0 + + if self['FileDate'] or self['DatePlayed']: + self['DatePlayed'] = (self['DatePlayed'] or self['FileDate']).split('.')[0].replace('T', " ") + + self['FileDate'] = "%s.%s.%s" % tuple(reversed(self['FileDate'].split('T')[0].split('-'))) + + def set(self): + return +# metadata = { +# 'title': self['Title'], +# 'genre': self['Genre'], +# 'year': self['Year'], +# 'album': self['Album'], +# 'artist': self['Artists'], +# 'rating': self['Rating'], +# 'comment': self['Comment'], +# 'date': self['FileDate'], +# 'mediatype': "music" +# } +## self.set_art() + +class PhotoAlbum(Photo): + def __init__(self, *args, **kwargs): + Photo.__init__(self, *args, **kwargs) + + def set(self): + metadata = {'title': self['Title']} + self.li.setProperty('path', self['Artwork']['Primary']) +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + self.li.setProperty('IsFolder', 'true') +# self.li.setIconImage('DefaultFolder.png') +# self.li.setProperty('IsPlayable', 'false') + self.li.setLabel(self['Title']) + self.li.setInfo('pictures', metadata) + self.li.setContentLookup(False) + +class Video(BaseListItem): + def __init__(self, *args, **kwargs): + BaseListItem.__init__(self, 'BrowseVideo', 'ArtworkParent', True, *args, **kwargs) + self.LOG = logging.getLogger("EMBY.code.listitem.Video") + + def format(self): + self['Genres'] = " / ".join(self['Genres'] or []) + self['Studios'] = [self.api.validate_studio(studio) for studio in (self['Studios'] or [])] + self['Studios'] = " / ".join(self['Studios']) + self['Mpaa'] = self.api.get_mpaa(self['Mpaa']) + self['People'] = self['People'] or [] + self['Countries'] = " / ".join(self['Countries'] or []) + self['Directors'] = " / ".join(self['Directors'] or []) + self['Writers'] = " / ".join(self['Writers'] or []) + self['Plot'] = self.api.get_overview(self['Plot']) + self['ShortPlot'] = self.api.get_overview(self['ShortPlot']) + self['DateAdded'] = self['DateAdded'].split('.')[0].replace('T', " ") + self['Rating'] = self['Rating'] or 0 + self['FileDate'] = "%s.%s.%s" % tuple(reversed(self['DateAdded'].split('T')[0].split('-'))) + self['Runtime'] = round(float((self['Runtime'] or 0) / 10000000.0), 6) + self['Resume'] = self.api.adjust_resume((self['Resume'] or 0) / 10000000.0, self.Utils) + self['PlayCount'] = self.api.get_playcount(self['Played'], self['PlayCount']) or 0 + self['Overlay'] = 7 if self['Played'] else 6 + self['Video'] = self.api.video_streams(self['Video'] or [], self['Container']) + self['Audio'] = self.api.audio_streams(self['Audio'] or []) + self['Streams'] = self.api.media_streams(self['Video'], self['Audio'], self['Subtitles']) + self['ChildCount'] = self['ChildCount'] or 0 + self['RecursiveCount'] = self['RecursiveCount'] or 0 + self['Unwatched'] = self['Unwatched'] or 0 + self['Artwork']['Backdrop'] = self['Artwork']['Backdrop'] or [] + self['Artwork']['Thumb'] = self['Artwork']['Thumb'] or "" + self['Artwork']['Primary'] = self['Artwork']['Primary'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + + if self['Premiere']: + self['Premiere'] = self['Premiere'].split('T')[0] + + if self['DatePlayed']: + self['DatePlayed'] = self['DatePlayed'].split('.')[0].replace('T', " ") + + def set(self): +## self.set_art() +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['Title'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'aired': self['Year'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "video", + 'lastplayed': self['DatePlayed'], + 'duration': self['Runtime'] + } + + if self.item['LI']['DbId']: + metadata['dbid'] = self.item['LI']['DbId'] + + self.li.setCast(self.api.get_actors()) + self.set_playable() + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + + def set_playable(self): + self.li.setProperty('totaltime', str(self['Runtime'])) + self.li.setProperty('IsPlayable', 'true') + self.li.setProperty('IsFolder', 'false') + + if self['Resume'] and self['Runtime'] and self.item['LI']['Seektime'] != False: + self.li.setProperty('resumetime', str(self['Resume'])) + self.li.setProperty('StartPercent', str(((self['Resume']/self['Runtime']) * 100))) + else: + self.li.setProperty('resumetime', '0') + self.li.setProperty('StartPercent', '0') + + for track in self['Streams']['video']: + self.li.addStreamInfo('video', { + 'duration': self['Runtime'], + 'aspect': track['aspect'], + 'codec': track['codec'], + 'width': track['width'], + 'height': track['height'] + }) + + for track in self['Streams']['audio']: + self.li.addStreamInfo('audio', {'codec': track['codec'], 'channels': track['channels']}) + + for track in self['Streams']['subtitle']: + self.li.addStreamInfo('subtitle', {'language': track}) + +class Audio(Music): + def __init__(self, *args, **kwargs): + Music.__init__(self, *args, **kwargs) + + def set(self): + metadata = { + 'title': self['Title'], + 'genre': self['Genre'], + 'year': self['Year'], + 'album': self['Album'], + 'artist': self['Artists'], + 'rating': self['Rating'], + 'comment': self['Comment'], + 'date': self['FileDate'], + 'mediatype': "song", + 'tracknumber': self['Index'], + 'discnumber': self['Disc'], + 'duration': self['Runtime'], + 'playcount': self['PlayCount'], + 'lastplayed': self['DatePlayed'], + 'musicbrainztrackid': self['UniqueId'] + } +## self.set_art() + self.li.setProperty('IsPlayable', 'true') + self.li.setProperty('IsFolder', 'false') + self.li.setLabel(self['Title']) + self.li.setInfo('music', metadata) + self.li.setContentLookup(False) + +class Album(Music): + def __init__(self, *args, **kwargs): + Music.__init__(self, *args, **kwargs) + + def set(self): + metadata = { + 'title': self['Title'], + 'genre': self['Genre'], + 'year': self['Year'], + 'album': self['Album'], + 'artist': self['Artists'], + 'rating': self['Rating'], + 'comment': self['Comment'], + 'date': self['FileDate'], + 'mediatype': "album", + 'musicbrainzalbumid': self['UniqueId'] + } +## self.set_art() + self.li.setLabel(self['Title']) + self.li.setInfo('music', metadata) + self.li.setContentLookup(False) + +class Artist(Music): + def __init__(self, *args, **kwargs): + Music.__init__(self, *args, **kwargs) + + def set(self): + metadata = { + 'title': self['Title'], + 'genre': self['Genre'], + 'year': self['Year'], + 'album': self['Album'], + 'artist': self['Artists'], + 'rating': self['Rating'], + 'comment': self['Comment'], + 'date': self['FileDate'], + 'mediatype': "artist", + 'musicbrainzartistid': self['UniqueId'] + } +## self.set_art() + self.li.setLabel(self['Title']) + self.li.setInfo('music', metadata) + self.li.setContentLookup(False) + +class Episode(Video): + def __init__(self, *args, **kwargs): + Video.__init__(self, *args, **kwargs) + + @classmethod + def art(cls): + return { + 'poster': "Series.Primary", + 'tvshow.poster': "Series.Primary", + 'clearart': "Art", + 'tvshow.clearart': "Art", + 'clearlogo': "Logo", + 'tvshow.clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb", + 'tvshow.landscape': "Thumb", + 'thumb': "Primary", + 'fanart': "Backdrop" + } + + def set(self): +## self.set_art() +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['OriginalTitle'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "episode", + 'tvshowtitle': self['SeriesName'], + 'season': self['Season'] or 0, + 'sortseason': self['Season'] or 0, + 'episode': self['Index'] or 0, + 'sortepisode': self['Index'] or 0, + 'lastplayed': self['DatePlayed'], + 'duration': self['Runtime'], + 'aired': self['Premiere'] + } + + if self.item['LI']['DbId']: + metadata['dbid'] = self.item['LI']['DbId'] + + self.li.setCast(self.api.get_actors()) + self.set_playable() + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class Season(Video): + def __init__(self, *args, **kwargs): + Video.__init__(self, *args, **kwargs) + + def set(self): +## self.set_art() +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['Title'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'aired': self['Year'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "season", + 'tvshowtitle': self['SeriesName'], + 'season': self['Index'] or 0, + 'sortseason': self['Index'] or 0 + } + + if self.item['LI']['DbId']: + metadata['dbid'] = self.item['LI']['DbId'] + + self.li.setCast(self.api.get_actors()) + self.li.setProperty('NumEpisodes', str(self['RecursiveCount'])) + self.li.setProperty('WatchedEpisodes', str(self['RecursiveCount'] - self['Unwatched'])) + self.li.setProperty('UnWatchedEpisodes', str(self['Unwatched'])) + self.li.setProperty('IsFolder', 'true') + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class Series(Video): + def __init__(self, *args, **kwargs): + Video.__init__(self, *args, **kwargs) + + def format(self): + super(Series, self).format() + + if self['Status'] != 'Ended': + self['Status'] = None + + def set(self): +## self.set_art() +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['OriginalTitle'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'aired': self['Year'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "tvshow", + 'tvshowtitle': self['Title'], + 'status': self['Status'] + } + + if self.item['LI']['DbId']: + metadata['dbid'] = self.item['LI']['DbId'] + + self.li.setCast(self.api.get_actors()) + self.li.setProperty('TotalSeasons', str(self['ChildCount'])) + self.li.setProperty('TotalEpisodes', str(self['RecursiveCount'])) + self.li.setProperty('WatchedEpisodes', str(self['RecursiveCount'] - self['Unwatched'])) + self.li.setProperty('UnWatchedEpisodes', str(self['Unwatched'])) + self.li.setProperty('IsFolder', 'true') + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class Movie(Video): + def __init__(self, *args, **kwargs): + Video.__init__(self, *args, **kwargs) + + def set(self): +## self.set_art() +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['OriginalTitle'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'aired': self['Year'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "movie", + 'imdbnumber': self['UniqueId'], + 'lastplayed': self['DatePlayed'], + 'duration': self['Runtime'], + 'userrating': self['CriticRating'] + } + + if self.item['LI']['DbId']: + metadata['dbid'] = self.item['LI']['DbId'] + + self.li.setCast(self.api.get_actors()) + self.set_playable() + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class BoxSet(Video): + def __init__(self, *args, **kwargs): + Video.__init__(self, *args, **kwargs) + + def set(self): +## self.set_art() +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['Title'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'aired': self['Year'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "set" + } + + if self.item['LI']['DbId']: + metadata['dbid'] = self.item['LI']['DbId'] + + self.li.setCast(self.api.get_actors()) + self.li.setProperty('IsFolder', 'true') + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class MusicVideo(Video): + def __init__(self, *args, **kwargs): + Video.__init__(self, *args, **kwargs) + + def set(self): +## self.set_art() +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['Title'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'aired': self['Year'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "musicvideo", + 'album': self['Album'], + 'artist': self['Artists'] or [], + 'lastplayed': self['DatePlayed'], + 'duration': self['Runtime'] + } + + if self.item['LI']['DbId']: + metadata['dbid'] = self.item['LI']['DbId'] + + self.li.setCast(self.api.get_actors()) + self.set_playable() + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class Intro(Video): + def __init__(self, *args, **kwargs): + Video.__init__(self, *args, **kwargs) + + def format(self): + self['Artwork']['Primary'] = self['Artwork']['Primary'] or self['Artwork']['Thumb'] or (self['Artwork']['Backdrop'][0] if len(self['Artwork']['Backdrop']) else "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg") + self['Artwork']['Primary'] += "&KodiCinemaMode=true" + self['Artwork']['Backdrop'] = [self['Artwork']['Primary']] + super(Intro, self).format() + + def set(self): +## self.set_art() + self.li.setArt({'poster': ""}) # Clear the poster value for intros / trailers to prevent issues in skins +# self.li.setIconImage('DefaultVideo.png') +# self.li.setThumbnailImage(self['Artwork']['Primary']) + self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) + metadata = { + 'title': self['Title'], + 'originaltitle': self['Title'], + 'sorttitle': self['SortTitle'], + 'country': self['Countries'], + 'genre': self['Genres'], + 'year': self['Year'], + 'rating': self['Rating'], + 'playcount': self['PlayCount'], + 'overlay': self['Overlay'], + 'director': self['Directors'], + 'mpaa': self['Mpaa'], + 'plot': self['Plot'], + 'plotoutline': self['ShortPlot'], + 'studio': self['Studios'], + 'tagline': self['Tagline'], + 'writer': self['Writers'], + 'premiered': self['Premiere'], + 'votes': self['Votes'], + 'dateadded': self['DateAdded'], + 'aired': self['Year'], + 'date': self['Premiere'] or self['FileDate'], + 'mediatype': "video", + 'lastplayed': self['DatePlayed'], + 'duration': self['Runtime'] + } + self.li.setCast(self.api.get_actors()) + self.set_playable() + self.li.setLabel(self['Title']) + self.li.setInfo('video', metadata) + self.li.setContentLookup(False) + +class Trailer(Intro): + def __init__(self, *args, **kwargs): + Intro.__init__(self, *args, **kwargs) + + def format(self): + self['Artwork']['Primary'] = self['Artwork']['Primary'] or self['Artwork']['Thumb'] or (self['Artwork']['Backdrop'][0] if len(self['Artwork']['Backdrop']) else "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg") + self['Artwork']['Primary'] += "&KodiTrailer=true" + self['Artwork']['Backdrop'] = [self['Artwork']['Primary']] + Video.format(self) + +MUSIC = { + 'Artist': Artist, + 'MusicArtist': Artist, + 'MusicAlbum': Album, + 'Audio': Audio, + 'Music': Music +} +PHOTO = { + 'Photo': Photo, + 'PhotoAlbum': PhotoAlbum +} +VIDEO = { + 'Episode': Episode, + 'Season': Season, + 'Series': Series, + 'Movie': Movie, + 'MusicVideo': MusicVideo, + 'BoxSet': BoxSet, + 'Trailer': Trailer, + 'AudioBook': Video, + 'Video': Video, + 'Intro': Intro +} +BASIC = { + 'Playlist': Playlist, + 'TvChannel': Channel +} + +#Translate an emby item into a Kodi listitem. +#Returns the listitem +class ListItem(): + def __init__(self, server_addr, Utils): + self.server = server_addr + self.Utils = Utils + + def _detect_type(self, item): + item_type = item['Type'] + + for typ in (VIDEO, MUSIC, PHOTO, BASIC): + if item_type in typ: + return typ[item_type] + + return VIDEO['Video'] + + def set(self, item, listitem, db_id, intro, seektime, *args, **kwargs): + listitem = listitem or xbmcgui.ListItem() + item['LI'] = { + 'DbId': db_id, + 'Seektime': seektime, + 'Server': self.server + } + + if intro: + func = VIDEO['Trailer'] if item['Type'] == 'Trailer' else VIDEO['Intro'] + else: + func = self._detect_type(item) + + func(listitem, item, self.Utils, *args, **kwargs) + item.pop('LI') + return listitem diff --git a/core/movies.py b/core/movies.py new file mode 100644 index 000000000..ccc56de9b --- /dev/null +++ b/core/movies.py @@ -0,0 +1,436 @@ +# -*- coding: utf-8 -*- +import logging + +import emby.downloader +import helper.wrapper +import helper.api +import database.emby_db +import database.queries +from . import obj_ops +from . import kodi +from . import queries_videos +from . import artwork +from . import common + +class Movies(): + def __init__(self, server, embydb, videodb, direct_path, Utils): + self.LOG = logging.getLogger("EMBY.core.movies.Movies") + self.Utils = Utils + self.server = server + self.emby = embydb + self.video = videodb + self.direct_path = direct_path + self.emby_db = database.emby_db.EmbyDatabase(embydb.cursor) + self.objects = obj_ops.Objects(self.Utils) + self.item_ids = [] + self.Downloader = emby.downloader.Downloader(self.Utils) + self.Common = common.Common(self.emby_db, self.objects, self.Utils, self.direct_path, self.server) + self.KodiDBIO = kodi.Kodi(videodb.cursor) + self.MoviesDBIO = MoviesDBIO(videodb.cursor) + self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.Utils) + + def __getitem__(self, key): + if key == 'Movie': + return self.movie + elif key == 'BoxSet': + return self.boxset + + + #If item does not exist, entry will be added. + #If item exists, entry will be updated + @helper.wrapper.stop + def movie(self, item, library=None): + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Movie') + obj['Item'] = item + obj['Library'] = library + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + update = True + StackedID = self.emby_db.get_stack(obj['PresentationKey']) or obj['Id'] + + if str(StackedID) != obj['Id']: + self.LOG.info("Skipping stacked movie %s [%s/%s]", obj['Title'], StackedID, obj['Id']) + Movies(self.server, self.emby, self.video, self.direct_path, self.Utils).remove(StackedID) + + try: + obj['MovieId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] + except TypeError: + update = False + self.LOG.debug("MovieId %s not found", obj['Id']) + obj['MovieId'] = self.MoviesDBIO.create_entry() + else: + if self.MoviesDBIO.get(*self.Utils.values(obj, queries_videos.get_movie_obj)) is None: + update = False + self.LOG.info("MovieId %s missing from kodi. repairing the entry.", obj['MovieId']) + + obj['Item']['MediaSources'][0] = self.objects.MapMissingData(obj['Item']['MediaSources'][0], 'MediaSources') + obj['MediaSourceID'] = obj['Item']['MediaSources'][0]['Id'] + obj['Runtime'] = obj['Item']['MediaSources'][0]['RunTimeTicks'] + + if obj['Item']['MediaSources'][0]['Path']: + obj['Path'] = obj['Item']['MediaSources'][0]['Path'] + + #don't use 3d movies as default + if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + for DataSource in obj['Item']['MediaSources']: + if not "3d" in self.Utils.StringMod(DataSource['Path']): + DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') + obj['Path'] = DataSource['Path'] + obj['MediaSourceID'] = DataSource['Id'] + obj['Runtime'] = DataSource['RunTimeTicks'] + break + + obj['Path'] = API.get_file_path(obj['Path']) + obj['Genres'] = obj['Genres'] or [] + obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['People'] = obj['People'] or [] + obj['Genre'] = " / ".join(obj['Genres']) + obj['Writers'] = " / ".join(obj['Writers'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Plot'] = API.get_overview(obj['Plot']) + obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['People'] = API.get_people_artwork(obj['People']) + obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['Premiered'] = self.Utils.convert_to_local(obj['Year']) if not obj['Premiered'] else self.Utils.convert_to_local(obj['Premiered']).replace(" ", "T").split('T')[0] + obj['DatePlayed'] = None if not obj['DatePlayed'] else self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) + obj['Audio'] = API.audio_streams(obj['Audio'] or []) + obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj = self.Common.get_path_filename(obj, "movies") + self.trailer(obj) + + if obj['Countries']: + self.MoviesDBIO.add_countries(*self.Utils.values(obj, queries_videos.update_country_obj)) + + tags = [] + tags.extend(obj['TagItems'] or obj['Tags'] or []) + tags.append(obj['LibraryName']) + + if obj['Favorite']: + tags.append('Favorite movies') + + obj['Tags'] = tags + + if update: + self.movie_update(obj) + else: + self.movie_add(obj) + + self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_movie_obj)) + self.KodiDBIO.update_file(*self.Utils.values(obj, queries_videos.update_file_obj)) + self.KodiDBIO.add_tags(*self.Utils.values(obj, queries_videos.add_tags_movie_obj)) + self.KodiDBIO.add_genres(*self.Utils.values(obj, queries_videos.add_genres_movie_obj)) + self.KodiDBIO.add_studios(*self.Utils.values(obj, queries_videos.add_studios_movie_obj)) + self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_movie_obj)) + self.KodiDBIO.add_streams(*self.Utils.values(obj, queries_videos.add_streams_obj)) + self.ArtworkDBIO.add(obj['Artwork'], obj['MovieId'], "movie") + self.item_ids.append(obj['Id']) + + if "StackTimes" in obj: + self.KodiDBIO.add_stacktimes(*self.Utils.values(obj, queries_videos.add_stacktimes_obj)) + + return not update + + #Add object to kodi + def movie_add(self, obj): + obj = self.Common.Streamdata_add(obj, False) + obj['RatingType'] = "default" + obj['RatingId'] = self.KodiDBIO.create_entry_rating() + self.KodiDBIO.add_ratings(*self.Utils.values(obj, queries_videos.add_rating_movie_obj)) + + if obj['CriticRating'] is not None: + self.KodiDBIO.add_ratings(*self.Utils.values(dict(obj, RatingId=self.KodiDBIO.create_entry_rating(), RatingType="tomatometerallcritics", Rating=float(obj['CriticRating']/10.0)), queries_videos.add_rating_movie_obj)) + + obj['Unique'] = self.MoviesDBIO.create_entry_unique_id() + self.MoviesDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_movie_obj)) + + for provider in obj['UniqueIds'] or {}: + unique_id = obj['UniqueIds'][provider] + provider = provider.lower() + + if provider != 'imdb': + temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.MoviesDBIO.create_entry_unique_id()) + self.MoviesDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_movie_obj)) + + obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.add_path_obj)) + obj['FileId'] = self.KodiDBIO.add_file(*self.Utils.values(obj, queries_videos.add_file_obj)) + self.MoviesDBIO.add(*self.Utils.values(obj, queries_videos.add_movie_obj)) + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_movie_obj)) + self.LOG.info("ADD movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + + #Update object to kodi + def movie_update(self, obj): + obj = self.Common.Streamdata_add(obj, True) + obj['RatingType'] = "default" + obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(obj, queries_videos.get_rating_movie_obj)) + self.KodiDBIO.update_ratings(*self.Utils.values(obj, queries_videos.update_rating_movie_obj)) + + if obj['CriticRating'] is not None: + temp_obj = dict(obj, RatingType="tomatometerallcritics", Rating=float(obj['CriticRating']/10.0)) + temp_obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(temp_obj, queries_videos.get_rating_movie_obj)) + self.KodiDBIO.update_ratings(*self.Utils.values(temp_obj, queries_videos.update_rating_movie_obj)) + + self.KodiDBIO.remove_unique_ids(*self.Utils.values(obj, queries_videos.delete_unique_ids_movie_obj)) + obj['Unique'] = self.MoviesDBIO.create_entry_unique_id() + self.MoviesDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_movie_obj)) + + for provider in obj['UniqueIds'] or {}: + unique_id = obj['UniqueIds'][provider] + provider = provider.lower() + + if provider != 'imdb': + temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.MoviesDBIO.create_entry_unique_id()) + self.MoviesDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_movie_obj)) + + self.MoviesDBIO.update(*self.Utils.values(obj, queries_videos.update_movie_obj)) + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + + def trailer(self, obj): + try: + if obj['LocalTrailer']: + trailer = self.server['api'].get_local_trailers(obj['Id']) + API = helper.api.API(trailer, self.Utils, self.server['auth/server-address']) + + if self.direct_path: + obj['Trailer'] = API.get_file_path(trailer[0]['Path']) + obj['Trailer'] = self.Utils.StringDecode(obj['Trailer']) + else: + obj['Trailer'] = "plugin://plugin.video.emby-next-gen/trailer?id=%s&mode=play" % trailer[0]['Id'] + elif obj['Trailer']: + obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1] + except Exception as error: + self.LOG.error("Failed to get trailer: %s", error) + obj['Trailer'] = None + + #If item does not exist, entry will be added. + #If item exists, entry will be updated. + #Process movies inside boxset. + #Process removals from boxset. + @helper.wrapper.stop + def boxset(self, item): + e_item = self.emby_db.get_item_by_id(item['Id']) + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Boxset') + obj['Overview'] = API.get_overview(obj['Overview']) + obj['Checksum'] = obj['Etag'] + + try: + obj['SetId'] = e_item[0] + self.MoviesDBIO.update_boxset(*self.Utils.values(obj, queries_videos.update_set_obj)) + except TypeError: + self.LOG.debug("SetId %s not found", obj['Id']) + obj['SetId'] = self.MoviesDBIO.add_boxset(*self.Utils.values(obj, queries_videos.add_set_obj)) + + self.boxset_current(obj) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + + for movie in obj['Current']: + temp_obj = dict(obj) + temp_obj['Movie'] = movie + temp_obj['MovieId'] = obj['Current'][temp_obj['Movie']] + self.MoviesDBIO.remove_from_boxset(*self.Utils.values(temp_obj, queries_videos.delete_movie_set_obj)) + self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.delete_parent_boxset_obj)) + self.LOG.info("DELETE from boxset [%s] %s: %s", temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId']) + + self.ArtworkDBIO.add(obj['Artwork'], obj['SetId'], "set") + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_boxset_obj)) + self.LOG.info("UPDATE boxset [%s] %s", obj['SetId'], obj['Title']) + + #Add or removes movies based on the current movies found in the boxset + def boxset_current(self, obj): + try: + current = self.emby_db.get_item_id_by_parent_id(*self.Utils.values(obj, database.queries.get_item_id_by_parent_boxset_obj)) + movies = dict(current) + except ValueError: + movies = {} + + obj['Current'] = movies + + for all_movies in self.Downloader.get_movies_by_boxset(obj['Id']): + for movie in all_movies['Items']: + temp_obj = dict(obj) + temp_obj['Title'] = movie['Name'] + temp_obj['Id'] = movie['Id'] + + try: + temp_obj['MovieId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except TypeError: + self.LOG.info("Failed to process %s to boxset.", temp_obj['Title']) + continue + + if temp_obj['Id'] not in obj['Current']: + self.MoviesDBIO.set_boxset(*self.Utils.values(temp_obj, queries_videos.update_movie_set_obj)) + self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.update_parent_movie_obj)) + self.LOG.info("ADD to boxset [%s/%s] %s: %s to boxset", temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['Id']) + else: + obj['Current'].pop(temp_obj['Id']) + + #Special function to remove all existing boxsets + def boxsets_reset(self): + boxsets = self.emby_db.get_items_by_media('set') + for boxset in boxsets: + self.remove(boxset[0]) + + #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + #Poster with progress bar + @helper.wrapper.stop + def userdata(self, item): + e_item = self.emby_db.get_item_by_id(item['Id']) + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'MovieUserData') + obj['Item'] = item + + try: + obj['MovieId'] = e_item[0] + obj['FileId'] = e_item[1] + except TypeError: + return + + obj = self.Common.Streamdata_add(obj, True) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + + if obj['DatePlayed']: + obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + if obj['Favorite']: + self.KodiDBIO.get_tag(*self.Utils.values(obj, queries_videos.get_tag_movie_obj)) + else: + self.KodiDBIO.remove_tag(*self.Utils.values(obj, queries_videos.delete_tag_movie_obj)) + + self.LOG.debug("New resume point %s: %s", obj['Id'], obj['Resume']) + self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA movie [%s/%s] %s: %s", obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + + #Remove movieid, fileid, emby reference. + #Remove artwork, boxset + @helper.wrapper.stop + def remove(self, item_id=None): + e_item = self.emby_db.get_item_by_id(item_id) + obj = {'Id': item_id} + + try: + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['Media'] = e_item[4] + except TypeError: + return + + self.ArtworkDBIO.delete(obj['KodiId'], obj['Media']) + + if obj['Media'] == 'movie': + self.MoviesDBIO.delete(*self.Utils.values(obj, queries_videos.delete_movie_obj)) + elif obj['Media'] == 'set': + for movie in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_movie_obj)): + temp_obj = dict(obj) + temp_obj['MovieId'] = movie[1] + temp_obj['Movie'] = movie[0] + self.MoviesDBIO.remove_from_boxset(*self.Utils.values(temp_obj, queries_videos.delete_movie_set_obj)) + self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.delete_parent_boxset_obj)) + + self.MoviesDBIO.delete_boxset(*self.Utils.values(obj, queries_videos.delete_set_obj)) + + self.emby_db.remove_item(item_id) + self.LOG.info("DELETE %s [%s/%s] %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id']) + +class MoviesDBIO(): + def __init__(self, cursor): + self.cursor = cursor + + def create_entry_unique_id(self): + self.cursor.execute(queries_videos.create_unique_id) + return self.cursor.fetchone()[0] + 1 + + def create_entry(self): + self.cursor.execute(queries_videos.create_movie) + return self.cursor.fetchone()[0] + 1 + + def create_entry_set(self): + self.cursor.execute(queries_videos.create_set) + return self.cursor.fetchone()[0] + 1 + + def create_entry_country(self): + self.cursor.execute(queries_videos.create_country) + return self.cursor.fetchone()[0] + 1 + + def get(self, *args): + try: + self.cursor.execute(queries_videos.get_movie, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def add(self, *args): + self.cursor.execute(queries_videos.add_movie, args) + + def update(self, *args): + self.cursor.execute(queries_videos.update_movie, args) + + def delete(self, kodi_id, file_id): + self.cursor.execute(queries_videos.delete_movie, (kodi_id,)) + self.cursor.execute(queries_videos.delete_file, (file_id,)) + +# def get_unique_id(self, *args): +# try: +# self.cursor.execute(queries_videos.get_unique_id, args) +# return self.cursor.fetchone()[0] +# except TypeError: +# return + + # Add the provider id, imdb, tvdb + def add_unique_id(self, *args): + self.cursor.execute(queries_videos.add_unique_id, args) + + # Update the provider id, imdb, tvdb +# def update_unique_id(self, *args): +# self.cursor.execute(queries_videos.update_unique_id, args) + + def add_countries(self, countries, *args): + for country in countries: + self.cursor.execute(queries_videos.update_country, (self.get_country(country),) + args) + + def add_country(self, *args): + country_id = self.create_entry_country() + self.cursor.execute(queries_videos.add_country, (country_id,) + args) + return country_id + + def get_country(self, *args): + try: + self.cursor.execute(queries_videos.get_country, args) + return self.cursor.fetchone()[0] + except TypeError: + return self.add_country(*args) + + def add_boxset(self, *args): + set_id = self.create_entry_set() + self.cursor.execute(queries_videos.add_set, (set_id,) + args) + return set_id + + def update_boxset(self, *args): + self.cursor.execute(queries_videos.update_set, args) + + def set_boxset(self, *args): + self.cursor.execute(queries_videos.update_movie_set, args) + + def remove_from_boxset(self, *args): + self.cursor.execute(queries_videos.delete_movie_set, args) + + def delete_boxset(self, *args): + self.cursor.execute(queries_videos.delete_set, args) diff --git a/core/music.py b/core/music.py new file mode 100644 index 000000000..0bc4e732a --- /dev/null +++ b/core/music.py @@ -0,0 +1,719 @@ +# -*- coding: utf-8 -*- +import datetime +import logging + +import database.queries +import database.emby_db +import helper.wrapper +import helper.api +from . import obj_ops +from . import queries_music +from . import artwork +from . import common + +class Music(): + def __init__(self, server, embydb, musicdb, direct_path, Utils): + self.LOG = logging.getLogger("EMBY.core.music.Music") + self.Utils = Utils + self.server = server + self.emby = embydb + self.music = musicdb + self.emby_db = database.emby_db.EmbyDatabase(self.emby.cursor) + self.objects = obj_ops.Objects(self.Utils) + self.item_ids = [] + self.DBVersion = int(self.Utils.window('kodidbverion.music')) + self.Common = common.Common(self.emby_db, self.objects, self.Utils, direct_path, self.server) + self.MusicDBIO = MusicDBIO(self.music.cursor, int(self.Utils.window('kodidbverion.music'))) + self.ArtworkDBIO = artwork.Artwork(musicdb.cursor, self.Utils) + + if not self.Utils.settings('MusicRescan.bool'): + self.MusicDBIO.disable_rescan() + self.Utils.settings('MusicRescan.bool', True) + + def __getitem__(self, key): + if key in ('MusicArtist', 'AlbumArtist'): + return self.artist + elif key == 'MusicAlbum': + return self.album + elif key == 'Audio': + return self.song + + #If item does not exist, entry will be added. + #If item exists, entry will be updated + @helper.wrapper.stop + def artist(self, item, library=None): + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Artist') + update = True + + try: + obj['ArtistId'] = e_item[0] + except TypeError: + update = False + obj['ArtistId'] = None + self.LOG.debug("ArtistId %s not found", obj['Id']) + else: + if self.MusicDBIO.validate_artist(*self.Utils.values(obj, queries_music.get_artist_by_id_obj)) is None: + update = False + self.LOG.info("ArtistId %s missing from kodi. repairing the entry.", obj['ArtistId']) + + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + obj['ArtistType'] = "MusicArtist" + obj['Genre'] = " / ".join(obj['Genres'] or []) + obj['Bio'] = API.get_overview(obj['Bio']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Thumb'] = obj['Artwork']['Primary'] + obj['Backdrops'] = obj['Artwork']['Backdrop'] or "" + + if obj['Thumb']: + obj['Thumb'] = "%s" % obj['Thumb'] + + if obj['Backdrops']: + obj['Backdrops'] = "%s" % obj['Backdrops'][0] + + if obj['DateAdded']: + obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + + if update: + self.artist_update(obj) + else: + self.artist_add(obj) + + if self.DBVersion >= 82: + self.MusicDBIO.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['LastScraped'], obj['SortName'], obj['DateAdded'], obj['ArtistId']) + else: + self.MusicDBIO.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['Backdrops'], obj['LastScraped'], obj['SortName'], obj['ArtistId']) + + self.ArtworkDBIO.add(obj['Artwork'], obj['ArtistId'], "artist") + self.item_ids.append(obj['Id']) + return not update + + #Add object to kodi + #safety checks: It looks like Emby supports the same artist multiple times. + #Kodi doesn't allow that. In case that happens we just merge the artist entries + def artist_add(self, obj): + obj['ArtistId'] = self.MusicDBIO.get(*self.Utils.values(obj, queries_music.get_artist_obj)) + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_artist_obj)) + self.LOG.info("ADD artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) + + #Update object to kodi + def artist_update(self, obj): + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) + + #Update object to kodi + @helper.wrapper.stop + def album(self, item, library=None): + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Album') + update = True + + try: + obj['AlbumId'] = e_item[0] + except TypeError: + update = False + obj['AlbumId'] = None + self.LOG.debug("AlbumId %s not found", obj['Id']) + else: + if self.MusicDBIO.validate_album(*self.Utils.values(obj, queries_music.get_album_by_id_obj)) is None: + update = False + + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['Rating'] = 0 + obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + obj['Genres'] = obj['Genres'] or [] + obj['Genre'] = " / ".join(obj['Genres']) + obj['Bio'] = API.get_overview(obj['Bio']) + obj['Artists'] = " / ".join(obj['Artists'] or []) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Thumb'] = obj['Artwork']['Primary'] + + if obj['DateAdded']: + obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + + if obj['Thumb']: + obj['Thumb'] = "%s" % obj['Thumb'] + + if update: + self.album_update(obj) + else: + self.album_add(obj) + + self.artist_link(obj) + self.artist_discography(obj) + + if self.DBVersion >= 82: + self.MusicDBIO.update_album(*self.Utils.values(obj, queries_music.update_album_obj82)) + else: + self.MusicDBIO.update_album(*self.Utils.values(obj, queries_music.update_album_obj)) + + self.ArtworkDBIO.add(obj['Artwork'], obj['AlbumId'], "album") + self.item_ids.append(obj['Id']) + return not update + + #Add object to kodi + def album_add(self, obj): + obj['AlbumId'] = self.MusicDBIO.get_album(*self.Utils.values(obj, queries_music.get_album_obj)) + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_album_obj)) + self.LOG.info("ADD album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) + + #Update object to kodi + def album_update(self, obj): + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) + + #Update the artist's discography + def artist_discography(self, obj): + for artist in (obj['ArtistItems'] or []): + temp_obj = dict(obj) + temp_obj['Id'] = artist['Id'] + temp_obj['AlbumId'] = obj['Id'] + + try: + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except TypeError: + continue + + self.MusicDBIO.add_discography(*self.Utils.values(temp_obj, queries_music.update_discography_obj)) + self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.update_parent_album_obj)) + + #Assign main artists to album. + #Artist does not exist in emby database, create the reference + def artist_link(self, obj): + for artist in (obj['AlbumArtists'] or []): + temp_obj = dict(obj) + temp_obj['Name'] = artist['Name'] + temp_obj['Id'] = artist['Id'] + + try: + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except TypeError: + try: + self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except Exception as error: + self.LOG.error(error) + continue + + self.MusicDBIO.update_artist_name(*self.Utils.values(temp_obj, queries_music.update_artist_name_obj)) + self.MusicDBIO.link(*self.Utils.values(temp_obj, queries_music.update_link_obj)) + self.item_ids.append(temp_obj['Id']) + + #Update object to kodi + @helper.wrapper.stop + def song(self, item, library=None): + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Song') + update = True + + try: + obj['SongId'] = e_item[0] + obj['PathId'] = e_item[2] + obj['AlbumId'] = e_item[3] + except TypeError: + update = False + obj['SongId'] = self.MusicDBIO.create_entry_song() + self.LOG.debug("SongId %s not found", obj['Id']) + else: + if self.MusicDBIO.validate_song(*self.Utils.values(obj, queries_music.get_song_by_id_obj)) is None: + update = False + + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['Path'] = API.get_file_path(obj['Path']) + obj = self.Common.get_path_filename(obj, "audio") + obj['Rating'] = 0 + obj['Genres'] = obj['Genres'] or [] + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Runtime'] = (obj['Runtime'] or 0) / 10000000.0 + obj['Genre'] = " / ".join(obj['Genres']) + obj['Artists'] = " / ".join(obj['Artists'] or []) + obj['AlbumArtists'] = obj['AlbumArtists'] or [] + obj['Index'] = obj['Index'] or 0 + obj['Disc'] = obj['Disc'] or 1 + obj['EmbedCover'] = False + obj['Comment'] = API.get_overview(obj['Comment']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Thumb'] = obj['Artwork']['Primary'] + + if obj['DateAdded']: + obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + + if obj['DatePlayed']: + obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + if obj['Disc'] != 1: + obj['Index'] = obj['Disc'] * 2 ** 16 + obj['Index'] + + if obj['Thumb']: + obj['Thumb'] = "%s" % obj['Thumb'] + + if update: + self.song_update(obj) + else: + self.song_add(obj) + + self.MusicDBIO.add_role(*self.Utils.values(obj, queries_music.update_role_obj)) # defaultt role + self.song_artist_link(obj) + self.song_artist_discography(obj) + obj['strAlbumArtists'] = " / ".join(obj['AlbumArtists']) + self.MusicDBIO.get_album_artist(*self.Utils.values(obj, queries_music.get_album_artist_obj)) + self.MusicDBIO.add_genres(*self.Utils.values(obj, queries_music.update_genre_song_obj)) + self.ArtworkDBIO.add(obj['Artwork'], obj['SongId'], "song") + self.item_ids.append(obj['Id']) + + if obj['SongAlbumId'] is None: + self.ArtworkDBIO.add(obj['Artwork'], obj['AlbumId'], "album") + + return not update + + #Add object to kodi. + #Verify if there's an album associated. + #If no album found, create a single's album + def song_add(self, obj): + obj['PathId'] = self.MusicDBIO.add_path(obj['Path']) + + try: + obj['AlbumId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_song_obj))[0] + except TypeError: + try: + if obj['SongAlbumId'] is None: + raise TypeError("No album id found associated?") + + self.album(self.server['api'].get_item(obj['SongAlbumId'])) + obj['AlbumId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_song_obj))[0] + except TypeError: + self.single(obj) + + self.MusicDBIO.add_song(*self.Utils.values(obj, queries_music.add_song_obj)) + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_song_obj)) + self.LOG.info("ADD song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + + #Update object to kodi + def song_update(self, obj): + self.MusicDBIO.update_path(*self.Utils.values(obj, queries_music.update_path_obj)) + self.MusicDBIO.update_song(*self.Utils.values(obj, queries_music.update_song_obj)) + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + + #Update the artist's discography + def song_artist_discography(self, obj): + artists = [] + + for artist in (obj['AlbumArtists'] or []): + temp_obj = dict(obj) + temp_obj['Name'] = artist['Name'] + temp_obj['Id'] = artist['Id'] + artists.append(temp_obj['Name']) + + try: + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except TypeError: + try: + self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except Exception as error: + self.LOG.error(error) + continue + + self.MusicDBIO.link(*self.Utils.values(temp_obj, queries_music.update_link_obj)) + self.item_ids.append(temp_obj['Id']) + + if obj['Album']: + temp_obj['Title'] = obj['Album'] + temp_obj['Year'] = 0 + self.MusicDBIO.add_discography(*self.Utils.values(temp_obj, queries_music.update_discography_obj)) + + obj['AlbumArtists'] = artists + + #Assign main artists to song. + #Artist does not exist in emby database, create the reference + def song_artist_link(self, obj): + for index, artist in enumerate(obj['ArtistItems'] or []): + temp_obj = dict(obj) + temp_obj['Name'] = artist['Name'] + temp_obj['Id'] = artist['Id'] + temp_obj['Index'] = index + + try: + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except TypeError: + try: + self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] + except Exception as error: + self.LOG.error(error) + continue + + self.MusicDBIO.link_song_artist(*self.Utils.values(temp_obj, queries_music.update_song_artist_obj)) + self.item_ids.append(temp_obj['Id']) + + def single(self, obj): + obj['AlbumId'] = self.MusicDBIO.create_entry_album() + obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + if self.DBVersion >= 82: + self.MusicDBIO.add_single(*self.Utils.values(obj, queries_music.add_single_obj82)) + else: + self.MusicDBIO.add_single(*self.Utils.values(obj, queries_music.add_single_obj)) + + #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + #Poster with progress bar + @helper.wrapper.stop + def userdata(self, item): + e_item = self.emby_db.get_item_by_id(item['Id']) + obj = self.objects.map(item, 'SongUserData') + + try: + obj['KodiId'] = e_item[0] + obj['Media'] = e_item[4] + except TypeError: + return + + obj['Rating'] = 0 + + if obj['Media'] == 'song': + if obj['DatePlayed']: + obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + self.MusicDBIO.rate_song(*self.Utils.values(obj, queries_music.update_song_rating_obj)) + + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA %s [%s] %s: %s", obj['Media'], obj['KodiId'], obj['Id'], obj['Title']) + + #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + #Poster with progress bar + #This should address single song scenario, where server doesn't actually create an album for the song + @helper.wrapper.stop + def remove(self, item_id): + e_item = self.emby_db.get_item_by_id(item_id) + obj = {'Id': item_id} + + try: + obj['KodiId'] = e_item[0] + obj['Media'] = e_item[4] + except TypeError: + return + + if obj['Media'] == 'song': + self.remove_song(obj['KodiId'], obj['Id']) + self.emby_db.remove_wild_item(obj['Id']) + + for item in self.emby_db.get_item_by_wild_id(*self.Utils.values(obj, database.queries.get_item_by_wild_obj)): + if item[1] == 'album': + temp_obj = dict(obj) + temp_obj['ParentId'] = item[0] + + if not self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): + self.remove_album(temp_obj['ParentId'], obj['Id']) + + elif obj['Media'] == 'album': + obj['ParentId'] = obj['KodiId'] + + for song in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_song_obj)): + self.remove_song(song[1], obj['Id']) + + self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_song_obj)) + self.remove_album(obj['KodiId'], obj['Id']) + elif obj['Media'] == 'artist': + obj['ParentId'] = obj['KodiId'] + + for album in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_album_obj)): + temp_obj = dict(obj) + temp_obj['ParentId'] = album[1] + + for song in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): + self.remove_song(song[1], obj['Id']) + + self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_song_obj)) + self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_artist_obj)) + self.remove_album(temp_obj['ParentId'], obj['Id']) + + self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_album_obj)) + self.remove_artist(obj['KodiId'], obj['Id']) + + self.emby_db.remove_item(*self.Utils.values(obj, database.queries.delete_item_obj)) + + def remove_artist(self, kodi_id, item_id): + self.ArtworkDBIO.delete(kodi_id, "artist") + self.MusicDBIO.delete(kodi_id) + self.LOG.info("DELETE artist [%s] %s", kodi_id, item_id) + + def remove_album(self, kodi_id, item_id): + self.ArtworkDBIO.delete(kodi_id, "album") + self.MusicDBIO.delete_album(kodi_id) + self.LOG.info("DELETE album [%s] %s", kodi_id, item_id) + + def remove_song(self, kodi_id, item_id): + self.ArtworkDBIO.delete(kodi_id, "song") + self.MusicDBIO.delete_song(kodi_id) + self.LOG.info("DELETE song [%s] %s", kodi_id, item_id) + + #Get all child elements from tv show emby id + def get_child(self, item_id): + e_item = self.emby_db.get_item_by_id(item_id) + obj = {'Id': item_id} + child = [] + + try: + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['ParentId'] = e_item[3] + obj['Media'] = e_item[4] + except TypeError: + return child + + obj['ParentId'] = obj['KodiId'] + + for album in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_album_obj)): + temp_obj = dict(obj) + temp_obj['ParentId'] = album[1] + child.append((album[0],)) + + for song in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): + child.append((song[0],)) + + return child + +class MusicDBIO(): + def __init__(self, cursor, MusicDBVersion): + self.LOG = logging.getLogger("EMBY.core.music.Music") + self.cursor = cursor + self.DBVersion = MusicDBVersion + + #Make sure rescan and kodi db set + def disable_rescan(self): + self.cursor.execute(queries_music.delete_rescan) + Data = [str(self.DBVersion), "0"] + self.cursor.execute(queries_music.disable_rescan, Data) + + #Leia has a dummy first entry + #idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing + def create_entry(self): + self.cursor.execute(queries_music.create_artist) + return self.cursor.fetchone()[0] + 1 + + def create_entry_album(self): + self.cursor.execute(queries_music.create_album) + return self.cursor.fetchone()[0] + 1 + + def create_entry_song(self): + self.cursor.execute(queries_music.create_song) + return self.cursor.fetchone()[0] + 1 + + def create_entry_genre(self): + self.cursor.execute(queries_music.create_genre) + return self.cursor.fetchone()[0] + 1 + + def update_path(self, *args): + self.cursor.execute(queries_music.update_path, args) + + def add_role(self, *args): + self.cursor.execute(queries_music.update_role, args) + + #Get artist or create the entry + def get(self, artist_id, name, musicbrainz): + try: + self.cursor.execute(queries_music.get_artist, (musicbrainz,)) + result = self.cursor.fetchone() + artist_id = result[0] + artist_name = result[1] + except TypeError: + artist_id = self.add_artist(artist_id, name, musicbrainz) + else: + if artist_name != name: + self.update_artist_name(artist_id, name) + + return artist_id + + #Safety check, when musicbrainz does not exist + def add_artist(self, artist_id, name, *args): + try: + self.cursor.execute(queries_music.get_artist_by_name, (name,)) + artist_id = self.cursor.fetchone()[0] + except TypeError: + artist_id = artist_id or self.create_entry() + self.cursor.execute(queries_music.add_artist, (artist_id, name,) + args) + + return artist_id + + def update_artist_name(self, *args): + self.cursor.execute(queries_music.update_artist_name, args) + + def update(self, *args): + if self.DBVersion >= 82: + self.cursor.execute(queries_music.update_artist82, args) + else: + self.cursor.execute(queries_music.update_artist, args) + + def link(self, *args): + self.cursor.execute(queries_music.update_link, args) + + def add_discography(self, *args): + self.cursor.execute(queries_music.update_discography, args) + + def validate_artist(self, *args): + try: + self.cursor.execute(queries_music.get_artist_by_id, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def validate_album(self, *args): + try: + self.cursor.execute(queries_music.get_album_by_id, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def validate_song(self, *args): + try: + self.cursor.execute(queries_music.get_song_by_id, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_album(self, album_id, name, musicbrainz, artists=None, *args): + try: + if musicbrainz is not None: + self.cursor.execute(queries_music.get_album, (musicbrainz,)) + album = None + else: + self.cursor.execute(queries_music.get_album_by_name, (name,)) + album = self.cursor.fetchone() + + album_id = (album or self.cursor.fetchone())[0] + except TypeError: + album_id = self.add_album(*(album_id, name, musicbrainz,) + args) + + return album_id + + def add_album(self, album_id, *args): + album_id = album_id or self.create_entry_album() + self.cursor.execute(queries_music.add_album, (album_id,) + args) + return album_id + + def update_album(self, *args): + if self.DBVersion >= 82: + self.cursor.execute(queries_music.update_album82, args) + else: + self.cursor.execute(queries_music.update_album, args) + + def get_album_artist(self, album_id, artists): + try: + self.cursor.execute(queries_music.get_album_artist, (album_id,)) + curr_artists = self.cursor.fetchone()[0] + except TypeError: + return + + if curr_artists != artists: + self.update_album_artist(artists, album_id) + + def update_album_artist(self, *args): + self.cursor.execute(queries_music.update_album_artist, args) + + def add_single(self, *args): + if self.DBVersion >= 82: + self.cursor.execute(queries_music.add_single82, args) + else: + self.cursor.execute(queries_music.add_single, args) + + def add_song(self, *args): + if self.DBVersion >= 82: + self.cursor.execute(queries_music.add_song82, args) + else: + self.cursor.execute(queries_music.add_song, args) + + def update_song(self, *args): + if self.DBVersion >= 82: + self.cursor.execute(queries_music.update_song82, args) + else: + self.cursor.execute(queries_music.update_song, args) + + def link_song_artist(self, *args): + self.cursor.execute(queries_music.update_song_artist, args) + +# def link_song_album(self, *args): +# self.cursor.execute(queries_music.update_song_album, args) + + def rate_song(self, *args): + self.cursor.execute(queries_music.update_song_rating, args) + + #Add genres, but delete current genres first + def add_genres(self, kodi_id, genres, media): + if media == 'album': + self.cursor.execute(queries_music.delete_genres_album, (kodi_id,)) + + for genre in genres: + genre_id = self.get_genre(genre) + self.cursor.execute(queries_music.update_genre_album, (genre_id, kodi_id)) + + elif media == 'song': + self.cursor.execute(queries_music.delete_genres_song, (kodi_id,)) + + for genre in genres: + genre_id = self.get_genre(genre) + self.cursor.execute(queries_music.update_genre_song, (genre_id, kodi_id)) + + def get_genre(self, *args): + try: + self.cursor.execute(queries_music.get_genre, args) + + return self.cursor.fetchone()[0] + except TypeError: + return self.add_genre(*args) + + def add_genre(self, *args): + genre_id = self.create_entry_genre() + self.cursor.execute(queries_music.add_genre, (genre_id,) + args) + return genre_id + + def delete(self, *args): + self.cursor.execute(queries_music.delete_artist, args) + + def delete_album(self, *args): + self.cursor.execute(queries_music.delete_album, args) + + def delete_song(self, *args): + self.cursor.execute(queries_music.delete_song, args) + + def get_path(self, *args): + try: + self.cursor.execute(queries_music.get_path, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def add_path(self, *args): + path_id = self.get_path(*args) + + if path_id is None: + path_id = self.create_entry_path() + self.cursor.execute(queries_music.add_path, (path_id,) + args) + + return path_id + + def create_entry_path(self): + self.cursor.execute(queries_music.create_path) + return self.cursor.fetchone()[0] + 1 diff --git a/core/musicvideos.py b/core/musicvideos.py new file mode 100644 index 000000000..7b6c78c56 --- /dev/null +++ b/core/musicvideos.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +import datetime +import logging +import re + +import database.queries +import database.emby_db +import helper.wrapper +import helper.api +from . import obj_ops +from . import kodi +from . import queries_videos +from . import artwork +from . import common + +class MusicVideos(): + def __init__(self, server, embydb, videodb, direct_path, Utils): + self.LOG = logging.getLogger("EMBY.core.musicvideos.MusicVideos") + self.Utils = Utils + self.server = server + self.emby = embydb + self.video = videodb + self.emby_db = database.emby_db.EmbyDatabase(embydb.cursor) + self.objects = obj_ops.Objects(self.Utils) + self.item_ids = [] + self.Common = common.Common(self.emby_db, self.objects, self.Utils, direct_path, self.server) + self.MusicVideosDBIO = MusicVideosDBIO(videodb.cursor) + self.KodiDBIO = kodi.Kodi(videodb.cursor) + self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.Utils) + + def __getitem__(self, key): + if key == 'MusicVideo': + return self.musicvideo + + @helper.wrapper.stop + def musicvideo(self, item, library=None): + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + + If we don't get the track number from Emby, see if we can infer it + from the sortname attribute. + ''' + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'MusicVideo') + obj['Item'] = item + obj['Library'] = library + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + update = True + + try: + obj['MvideoId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] + except TypeError: + update = False + self.LOG.debug("MvideoId for %s not found", obj['Id']) + obj['MvideoId'] = self.MusicVideosDBIO.create_entry() + else: + if self.MusicVideosDBIO.get(*self.Utils.values(obj, queries_videos.get_musicvideo_obj)) is None: + update = False + self.LOG.info("MvideoId %s missing from kodi. repairing the entry.", obj['MvideoId']) + + obj['Item']['MediaSources'][0] = self.objects.MapMissingData(obj['Item']['MediaSources'][0], 'MediaSources') + obj['MediaSourceID'] = obj['Item']['MediaSources'][0]['Id'] + obj['Runtime'] = obj['Item']['MediaSources'][0]['RunTimeTicks'] + + if obj['Item']['MediaSources'][0]['Path']: + obj['Path'] = obj['Item']['MediaSources'][0]['Path'] + + #don't use 3d movies as default + if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + for DataSource in obj['Item']['MediaSources']: + if not "3d" in self.Utils.StringMod(DataSource['Path']): + DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') + obj['Path'] = DataSource['Path'] + obj['MediaSourceID'] = DataSource['Id'] + obj['Runtime'] = DataSource['RunTimeTicks'] + break + + obj['Path'] = API.get_file_path(obj['Path']) + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['Genres'] = obj['Genres'] or [] + obj['ArtistItems'] = obj['ArtistItems'] or [] + obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Plot'] = API.get_overview(obj['Plot']) + obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = None if not obj['DatePlayed'] else self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['Premiere'] = self.Utils.convert_to_local(obj['Premiere']) if obj['Premiere'] else datetime.date(int(str(obj['Year'])[:4]) if obj['Year'] else 2021, 1, 1) + obj['Genre'] = " / ".join(obj['Genres']) + obj['Studio'] = " / ".join(obj['Studios']) + obj['Artists'] = " / ".join(obj['Artists'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) + obj['Audio'] = API.audio_streams(obj['Audio'] or []) + obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj = self.Common.get_path_filename(obj, "musicvideos") + + if obj['Premiere']: + obj['Premiere'] = str(obj['Premiere']).split('.')[0].replace('T', " ") + + for artist in obj['ArtistItems']: + artist['Type'] = "Artist" + + obj['People'] = obj['People'] or [] + obj['ArtistItems'] + obj['People'] = API.get_people_artwork(obj['People']) + + if obj['Index'] is None and obj['SortTitle'] is not None: + search = re.search(r'^\d+\s?', obj['SortTitle']) + + if search: + obj['Index'] = search.group() + + tags = [] + tags.extend(obj['TagItems'] or obj['Tags'] or []) + tags.append(obj['LibraryName']) + + if obj['Favorite']: + tags.append('Favorite musicvideos') + + obj['Tags'] = tags + + if update: + self.musicvideo_update(obj) + else: + self.musicvideo_add(obj) + + self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_mvideo_obj)) + self.KodiDBIO.update_file(*self.Utils.values(obj, queries_videos.update_file_obj)) + self.KodiDBIO.add_tags(*self.Utils.values(obj, queries_videos.add_tags_mvideo_obj)) + self.KodiDBIO.add_genres(*self.Utils.values(obj, queries_videos.add_genres_mvideo_obj)) + self.KodiDBIO.add_studios(*self.Utils.values(obj, queries_videos.add_studios_mvideo_obj)) + self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_mvideo_obj)) + self.KodiDBIO.add_streams(*self.Utils.values(obj, queries_videos.add_streams_obj)) + self.ArtworkDBIO.add(obj['Artwork'], obj['MvideoId'], "musicvideo") + self.item_ids.append(obj['Id']) + + if "StackTimes" in obj: + self.KodiDBIO.add_stacktimes(*self.Utils.values(obj, queries_videos.add_stacktimes_obj)) + + return not update + + #Add object to kodi + def musicvideo_add(self, obj): + obj = self.Common.Streamdata_add(obj, False) + obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.add_path_obj)) + obj['FileId'] = self.KodiDBIO.add_file(*self.Utils.values(obj, queries_videos.add_file_obj)) + self.MusicVideosDBIO.add(*self.Utils.values(obj, queries_videos.add_musicvideo_obj)) + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_mvideo_obj)) + self.LOG.info("ADD mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + + #Update object to kodi + def musicvideo_update(self, obj): + obj = self.Common.Streamdata_add(obj, True) + self.MusicVideosDBIO.update(*self.Utils.values(obj, queries_videos.update_musicvideo_obj)) + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + + @helper.wrapper.stop + def userdata(self, item): + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + ''' + e_item = self.emby_db.get_item_by_id(item['Id']) + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'MusicVideoUserData') + obj['Item'] = item + + try: + obj['MvideoId'] = e_item[0] + obj['FileId'] = e_item[1] + except TypeError: + return + + obj = self.Common.Streamdata_add(obj, True) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + + if obj['DatePlayed']: + obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + if obj['Favorite']: + self.KodiDBIO.get_tag(*self.Utils.values(obj, queries_videos.get_tag_mvideo_obj)) + else: + self.KodiDBIO.remove_tag(*self.Utils.values(obj, queries_videos.delete_tag_mvideo_obj)) + + self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA mvideo [%s/%s] %s: %s", obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + + #Remove mvideoid, fileid, pathid, emby reference + @helper.wrapper.stop + def remove(self, item_id): + e_item = self.emby_db.get_item_by_id(item_id) + obj = {'Id': item_id} + + try: + obj['MvideoId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] + except TypeError: + return + + self.ArtworkDBIO.delete(obj['MvideoId'], "musicvideo") + self.MusicVideosDBIO.delete(*self.Utils.values(obj, queries_videos.delete_musicvideo_obj)) + self.emby_db.remove_item(*self.Utils.values(obj, database.queries.delete_item_obj)) + self.LOG.info("DELETE musicvideo %s [%s/%s] %s", obj['MvideoId'], obj['PathId'], obj['FileId'], obj['Id']) + + +class MusicVideosDBIO(): + def __init__(self, cursor): + self.cursor = cursor + + def create_entry(self): + self.cursor.execute(queries_videos.create_musicvideo) + return self.cursor.fetchone()[0] + 1 + + def get(self, *args): + try: + self.cursor.execute(queries_videos.get_musicvideo, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def add(self, *args): + self.cursor.execute(queries_videos.add_musicvideo, args) + + def update(self, *args): + self.cursor.execute(queries_videos.update_musicvideo, args) + + def delete(self, kodi_id, file_id): + self.cursor.execute(queries_videos.delete_musicvideo, (kodi_id,)) + self.cursor.execute(queries_videos.delete_file, (file_id,)) diff --git a/core/obj_map.json b/core/obj_map.json new file mode 100644 index 000000000..bde0f1c3a --- /dev/null +++ b/core/obj_map.json @@ -0,0 +1,487 @@ +{ + "video": "special://database/MyVideos119.db", + "music": "special://database/MyMusic82.db", + "texture": "special://database/Textures13.db", + "emby": "special://database/emby.db", + "MovieProviderName": "imdb", + "Movie": { + "Id": "Id", + "Title": "Name", + "SortTitle": "SortName", + "Path": "Path", + "Genres": "Genres", + "UniqueId": "ProviderIds/Imdb", + "UniqueIds": "ProviderIds", + "Rating": "CommunityRating", + "Year": "ProductionYear", + "Votes": "VoteCount", + "Plot": "Overview", + "ShortPlot": "ShortOverview", + "People": "People", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Cast": "People:?Type=Actor$Name", + "Tagline": "Taglines/0", + "Mpaa": "OfficialRating", + "Country": "ProductionLocations/0", + "Countries": "ProductionLocations", + "Studios": "Studios:?$Name", + "Studio": "Studios/0/Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "LocalTrailer": "LocalTrailerCount", + "Trailer": "RemoteTrailers/0/Url", + "DateAdded": "DateCreated", + "Premiered": "PremiereDate", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Favorite": "UserData/IsFavorite", + "Resume": "UserData/PlaybackPositionTicks", + "Tags": "Tags", + "TagItems": "TagItems:?$Name", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "EmbyParentId": "ParentId", + "CriticRating": "CriticRating", + "PresentationKey": "PresentationUniqueKey", + "OriginalTitle": "OriginalTitle" + }, + "MovieUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Played": "UserData/Played", + "PresentationKey": "PresentationUniqueKey" + }, + "Boxset": { + "Id": "Id", + "Title": "Name", + "Overview": "Overview", + "PresentationKey": "PresentationUniqueKey", + "Etag": "Etag" + }, + "SeriesProviderName": "tvdb", + "Series": { + "Id": "Id", + "Title": "Name", + "SortTitle": "SortName", + "People": "People", + "Path": "Path", + "Genres": "Genres", + "Plot": "Overview", + "Rating": "CommunityRating", + "Year": "ProductionYear", + "Votes": "VoteCount", + "Premiere": "PremiereDate", + "UniqueId": "ProviderIds/Tvdb", + "UniqueIds": "ProviderIds", + "Mpaa": "OfficialRating", + "Studios": "Studios:?$Name", + "Tags": "Tags", + "TagItems": "TagItems:?$Name", + "Favorite": "UserData/IsFavorite", + "RecursiveCount": "RecursiveItemCount", + "EmbyParentId": "ParentId", + "Status": "Status", + "PresentationKey": "PresentationUniqueKey", + "OriginalTitle": "OriginalTitle" + }, + "Season": { + "Id": "Id", + "Index": "IndexNumber", + "SeriesId": "SeriesId", + "Location": "LocationType", + "Title": "Name", + "PresentationKey": "PresentationUniqueKey" + }, + "EpisodeProviderName": "tvdb", + "Episode": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "Plot": "Overview", + "People": "People", + "Rating": "CommunityRating", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Premiere": "PremiereDate", + "Votes": "VoteCount", + "UniqueId": "ProviderIds/Tvdb", + "UniqueIds": "ProviderIds", + "SeriesId": "SeriesId", + "Season": "ParentIndexNumber", + "Index": "IndexNumber", + "AbsoluteNumber": "AbsoluteEpisodeNumber", + "AirsAfterSeason": "AirsAfterSeasonNumber", + "AirsBeforeSeason": "AirsBeforeSeasonNumber,SortParentIndexNumber", + "AirsBeforeEpisode": "AirsBeforeEpisodeNumber,SortIndexNumber", + "MultiEpisode": "IndexNumberEnd", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DateAdded": "DateCreated", + "DatePlayed": "UserData/LastPlayedDate", + "Resume": "UserData/PlaybackPositionTicks", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "Location": "LocationType", + "EmbyParentId": "SeriesId,ParentId", + "PresentationKey": "PresentationUniqueKey", + "OriginalTitle": "OriginalTitle" + }, + "EpisodeUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PresentationKey": "PresentationUniqueKey" + }, + "MusicVideo": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "DateAdded": "DateCreated", + "DatePlayed": "UserData/LastPlayedDate", + "PlayCount": "UserData/PlayCount", + "Resume": "UserData/PlaybackPositionTicks", + "SortTitle": "SortName", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Plot": "Overview", + "Year": "ProductionYear", + "Premiere": "PremiereDate", + "Genres": "Genres", + "Studios": "Studios?$Name", + "Artists": "ArtistItems:?$Name", + "ArtistItems": "ArtistItems", + "Album": "Album", + "Index": "Track", + "People": "People", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "Tags": "Tags", + "TagItems": "TagItems:?$Name", + "Played": "UserData/Played", + "Favorite": "UserData/IsFavorite", + "Directors": "People:?Type=Director$Name", + "EmbyParentId": "ParentId", + "PresentationKey": "PresentationUniqueKey" + }, + "MusicVideoUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Played": "UserData/Played" + }, + "Artist": { + "Id": "Id", + "Name": "Name", + "UniqueId": "ProviderIds/MusicBrainzArtist", + "Genres": "Genres", + "Bio": "Overview", + "EmbyParentId": "ParentId", + "DateAdded": "DateCreated", + "SortName": "SortName", + "PresentationKey": "PresentationUniqueKey" + }, + "Album": { + "Id": "Id", + "Title": "Name", + "UniqueId": "ProviderIds/MusicBrainzAlbum", + "Year": "ProductionYear", + "Genres": "Genres", + "Bio": "Overview", + "AlbumArtists": "AlbumArtists", + "Artists": "AlbumArtists:?$Name", + "ArtistItems": "ArtistItems", + "EmbyParentId": "ParentId", + "DateAdded": "DateCreated", + "PresentationKey": "PresentationUniqueKey" + }, + "Song": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "UniqueId": "ProviderIds/MusicBrainzTrackId", + "Genres": "Genres", + "Artists": "ArtistItems:?$Name", + "Index": "IndexNumber", + "Disc": "ParentIndexNumber", + "Year": "ProductionYear", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Comment": "Overview", + "ArtistItems": "ArtistItems", + "AlbumArtists": "AlbumArtists", + "Album": "Album", + "SongAlbumId": "AlbumId", + "Container": "MediaSources/0/Container", + "EmbyParentId": "ParentId", + "PresentationKey": "PresentationUniqueKey" + }, + "SongUserData": { + "Id": "Id", + "Title": "Name", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PresentationKey": "PresentationUniqueKey" + }, + "Artwork": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags" + }, + "ArtworkParent": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags", + "ParentBackdropId": "ParentBackdropItemId", + "ParentBackdropTags": "ParentBackdropImageTags", + "ParentLogoId": "ParentLogoItemId", + "ParentLogoTag": "ParentLogoImageTag", + "ParentArtId": "ParentArtItemId", + "ParentArtTag": "ParentArtImageTag", + "ParentThumbId": "ParentThumbItemId", + "ParentThumbTag": "ParentThumbTag", + "SeriesTag": "SeriesPrimaryImageTag", + "SeriesId": "SeriesId" + }, + "ArtworkMusic": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags", + "ParentBackdropId": "ParentBackdropItemId", + "ParentBackdropTags": "ParentBackdropImageTags", + "ParentLogoId": "ParentLogoItemId", + "ParentLogoTag": "ParentLogoImageTag", + "ParentArtId": "ParentArtItemId", + "ParentArtTag": "ParentArtImageTag", + "ParentThumbId": "ParentThumbItemId", + "ParentThumbTag": "ParentThumbTag", + "AlbumId": "AlbumId", + "AlbumTag": "AlbumPrimaryImageTag" + }, + "BrowseVideo": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Plot": "Overview", + "Year": "ProductionYear", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Cast": "People:?Type=Actor$Name", + "Mpaa": "OfficialRating", + "Genres": "Genres", + "Studios": "Studios:?$Name,SeriesStudio", + "Premiere": "PremiereDate,DateCreated", + "Rating": "CommunityRating", + "Votes": "VoteCount", + "Season": "ParentIndexNumber", + "Index": "IndexNumber,AbsoluteEpisodeNumber", + "SeriesName": "SeriesName", + "Countries": "ProductionLocations", + "Played": "UserData/Played", + "People": "People", + "ShortPlot": "ShortOverview", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Tagline": "Taglines/0", + "UniqueId": "ProviderIds/Imdb", + "DatePlayed": "UserData/LastPlayedDate", + "Artists": "ArtistItems:?$Name", + "Album": "Album", + "Votes": "VoteCount", + "Path": "Path", + "LocalTrailer": "LocalTrailerCount", + "Trailer": "RemoteTrailers/0/Url", + "DateAdded": "DateCreated", + "SortTitle": "SortName", + "PlayCount": "UserData/PlayCount", + "Resume": "UserData/PlaybackPositionTicks", + "Subtitles": "MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaStreams:?Type=Audio", + "Video": "MediaStreams:?Type=Video", + "Container": "Container", + "Unwatched": "UserData/UnplayedItemCount", + "ChildCount": "ChildCount", + "RecursiveCount": "RecursiveItemCount", + "MediaType": "MediaType", + "CriticRating": "CriticRating", + "Status": "Status", + "OriginalTitle": "OriginalTitle" + }, + "BrowseAudio": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Index": "IndexNumber", + "Disc": "ParentIndexNumber", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Year": "ProductionYear", + "Genre": "Genres/0", + "Album": "Album", + "Artists": "ArtistItems/0/Name", + "Rating": "CommunityRating", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "UniqueId": "ProviderIds/MusicBrainzTrackId,ProviderIds/MusicBrainzAlbum,ProviderIds/MusicBrainzArtist", + "Comment": "Overview", + "FileDate": "DateCreated", + "Played": "UserData/Played" + }, + "BrowsePhoto": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "FileDate": "DateCreated", + "Width": "Width", + "Height": "Height", + "Size": "Size", + "Overview": "Overview", + "CameraMake": "CameraMake", + "CameraModel": "CameraModel", + "ExposureTime": "ExposureTime", + "FocalLength": "FocalLength" + }, + "BrowseFolder": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Overview": "Overview" + }, + "BrowseChannel": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "ProgramName": "CurrentProgram/Name", + "Played": "CurrentProgram/UserData/Played", + "PlayCount": "CurrentProgram/UserData/PlayCount", + "Runtime": "CurrentProgram/RunTimeTicks", + "MediaType": "MediaType" + }, + "MediaSources": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "Protocol": "Protocol", + "Id": "Id", + "Path": "Path", + "Type": "Type", + "Container": "Container", + "Size": "Size", + "Name": "Name", + "IsRemote": "IsRemote", + "RunTimeTicks": "RunTimeTicks", + "SupportsTranscoding": "SupportsTranscoding", + "SupportsDirectStream": "SupportsDirectStream", + "SupportsDirectPlay": "SupportsDirectPlay", + "IsInfiniteStream": "IsInfiniteStream", + "RequiresOpening": "RequiresOpening", + "RequiresClosing": "RequiresClosing", + "RequiresLooping": "RequiresLooping", + "SupportsProbing": "SupportsProbing", + "Formats": "Formats", + "Bitrate": "Bitrate", + "RequiredHttpHeaders": "RequiredHttpHeaders", + "ReadAtNativeFramerate": "ReadAtNativeFramerate", + "DefaultAudioStreamIndex": "DefaultAudioStreamIndex" + }, + "AudioStreams": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "AudioIndex": "AudioIndex", + "StreamIndex": "StreamIndex", + "Codec": "Codec", + "Language": "Language", + "TimeBase": "TimeBase", + "CodecTimeBase": "CodecTimeBase", + "DisplayTitle": "DisplayTitle", + "DisplayLanguage": "DisplayLanguage", + "IsInterlaced": "IsInterlaced", + "ChannelLayout": "ChannelLayout", + "BitRate": "BitRate", + "Channels": "Channels", + "SampleRate": "SampleRate", + "IsDefault": "IsDefault", + "IsForced": "IsForced", + "Profile": "Profile", + "Type": "Type", + "IsExternal": "IsExternal", + "IsTextSubtitleStream": "IsTextSubtitleStream", + "SupportsExternalStream": "SupportsExternalStream", + "Protocol": "Protocol" + }, + "VideoStreams": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "VideoIndex": "VideoIndex", + "StreamIndex": "StreamIndex", + "Codec": "Codec", + "TimeBase": "TimeBase", + "CodecTimeBase": "CodecTimeBase", + "VideoRange": "VideoRange", + "DisplayTitle": "DisplayTitle", + "IsInterlaced": "IsInterlaced", + "BitRate": "BitRate", + "BitDepth": "BitDepth", + "RefFrames": "RefFrames", + "IsDefault": "IsDefault", + "IsForced": "IsForced", + "Height": "Height", + "Width": "Width", + "AverageFrameRate": "AverageFrameRate", + "RealFrameRate": "RealFrameRate", + "Profile": "Profile", + "Type": "Type", + "AspectRatio": "AspectRatio", + "IsExternal": "IsExternal", + "IsTextSubtitleStream": "IsTextSubtitleStream", + "SupportsExternalStream": "SupportsExternalStream", + "Protocol": "Protocol", + "PixelFormat": "PixelFormat", + "Level": "Level", + "IsAnamorphic": "IsAnamorphic" + }, + "Subtitles": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "SubtitleIndex": "SubtitleIndex", + "StreamIndex": "StreamIndex", + "IsForced": "IsForced", + "IsInterlaced": "IsInterlaced", + "DisplayTitle": "DisplayTitle", + "SupportsExternalStream": "SupportsExternalStream", + "Language": "Language", + "DisplayLanguage": "DisplayLanguage", + "Codec": "Codec", + "CodecTimeBase": "CodecTimeBase", + "Protocol": "Protocol", + "Type": "Type", + "Path": "Path", + "TimeBase": "TimeBase", + "IsTextSubtitleStream": "IsTextSubtitleStream", + "IsDefault": "IsDefault", + "IsExternal": "IsExternal" + } +} diff --git a/core/obj_ops.py b/core/obj_ops.py new file mode 100644 index 000000000..cc6e0235f --- /dev/null +++ b/core/obj_ops.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +import json +import os + +class Objects(): + def __init__(self, Utils): + self.Utils = Utils + + if not self.Utils.window('emby_objects.mapping'): + infile = open(os.path.join(os.path.dirname(__file__), 'obj_map.json'), 'r') + Value = infile.read() + self.Utils.window('emby_objects.mapping', value=Value) + infile.close() + + self.objects = json.loads(self.Utils.window('emby_objects.mapping')) + self.mapped_item = {} + + def MapMissingData(self, item, mapping_name): + mapping = self.objects[mapping_name] + + for key, value in list(mapping.items()): + if not key in item: + item[key] = "" + + return item + + def map(self, item, mapping_name): + ''' Syntax to traverse the item dictionary. + This of the query almost as a url. + + Item is the Emby item json object structure + + ",": each element will be used as a fallback until a value is found. + "?": split filters and key name from the query part, i.e. MediaSources/0?$Name + "$": lead the key name with $. Only one key value can be requested per element. + ":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name + MediaStreams is a list. + "/": indicates where to go directly + ''' + self.mapped_item = {} + + if not mapping_name: + raise Exception("execute mapping() first") + + mapping = self.objects[mapping_name] + + for key, value in list(mapping.items()): + self.mapped_item[key] = None + params = value.split(',') + + for param in params: + obj = item + obj_param = param + obj_key = "" + obj_filters = {} + + if '?' in obj_param: + + if '$' in obj_param: + obj_param, obj_key = obj_param.rsplit('$', 1) + + obj_param, filters = obj_param.rsplit('?', 1) + + if filters: + for filterData in filters.split('&'): + filter_key, filter_value = filterData.split('=') + obj_filters[filter_key] = filter_value + + if ':' in obj_param: + result = [] + + for d in self.__recursiveloop__(obj, obj_param): + + if obj_filters and self.__filters__(d, obj_filters): + result.append(d) + elif not obj_filters: + result.append(d) + + obj = result + obj_filters = {} + elif '/' in obj_param: + obj = self.__recursive__(obj, obj_param) + elif obj is item and obj is not None: + obj = item.get(obj_param) + + if obj_filters and obj: + if not self.__filters__(obj, obj_filters): + obj = None + + if obj is None and len(params) != params.index(param): + continue + + if obj_key: + obj = [d[obj_key] for d in obj if d.get(obj_key)] if type(obj) == list else obj.get(obj_key) + + self.mapped_item[key] = obj + break + + if not mapping_name.startswith('Browse') and not mapping_name.startswith('Artwork') and not mapping_name.startswith('MediaSources') and not mapping_name.startswith('AudioStreams') and not mapping_name.startswith('VideoStreams'): + self.mapped_item['ProviderName'] = self.objects.get('%sProviderName' % mapping_name) + self.mapped_item['Checksum'] = json.dumps(item['UserData']) + self.mapped_item.setdefault('PresentationKey', None) + + return self.mapped_item + + def __recursiveloop__(self, obj, keys): + first, rest = keys.split(':', 1) + obj = self.__recursive__(obj, first) + + if obj: + if rest: + for item in obj: + self.__recursiveloop__(item, rest) + else: + for item in obj: + yield item + + def __recursive__(self, obj, keys): + for string in keys.split('/'): + if not obj: + return + + obj = obj[int(string)] if string.isdigit() else obj.get(string) + + return obj + + def __filters__(self, obj, filters): + result = False + + for key, value in iter(list(filters.items())): + inverse = False + + if value.startswith('!'): + inverse = True + value = value.split('!', 1)[1] + + if value.lower() == "null": + value = None + + result = obj.get(key) != value if inverse else obj.get(key) == value + + return result diff --git a/core/queries_music.py b/core/queries_music.py new file mode 100644 index 000000000..2b2a3941b --- /dev/null +++ b/core/queries_music.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +create_artist = """ SELECT coalesce(max(idArtist), 1) FROM artist """ +create_album = """ SELECT coalesce(max(idAlbum), 0) FROM album """ +create_song = """ SELECT coalesce(max(idSong), 0) FROM song """ +create_genre = """ SELECT coalesce(max(idGenre), 0) FROM genre """ +get_artist = """ SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = ? """ +get_artist_obj = ["{ArtistId}", "{Name}", "{UniqueId}"] +get_artist_by_name = """ SELECT idArtist FROM artist WHERE strArtist = ? COLLATE NOCASE """ +get_artist_by_id = """ SELECT * FROM artist WHERE idArtist = ? """ +get_artist_by_id_obj = ["{ArtistId}"] +get_album_by_id = """ SELECT * FROM album WHERE idAlbum = ? """ +get_album_by_id_obj = ["{AlbumId}"] +get_song_by_id = """ SELECT * FROM song WHERE idSong = ? """ +get_song_by_id_obj = ["{SongId}"] +get_album = """ SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = ? """ +get_album_obj = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album"] +get_album_by_name = """ SELECT idAlbum, strArtistDisp FROM album WHERE strAlbum = ? """ +get_album_artist = """ SELECT strArtistDisp FROM album WHERE idAlbum = ? """ +get_album_artist_obj = ["{AlbumId}", "{strAlbumArtists}"] +get_genre = """ SELECT idGenre FROM genre WHERE strGenre = ? COLLATE NOCASE """ +get_total_episodes = """ SELECT totalCount FROM tvshowcounts WHERE idShow = ? """ +get_artwork = """ SELECT url FROM art """ +add_artist = """ INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) VALUES (?, ?, ?) """ +add_album = """ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) VALUES (?, ?, ?, ?) """ +add_single = """ INSERT INTO album(strArtistDisp, iYear, strGenres, strImage, iUserrating, lastScraped, strReleaseType) VALUES (?, ?, ?, ?, ?, ?, ?) """ +add_single_obj = ["{Artists}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "single"] +add_single82 = """ INSERT INTO album(strArtistDisp, strReleaseDate, strGenres, strImage, iUserrating, lastScraped, bScrapedMBID, strReleaseType, dateAdded) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?) """ +add_single_obj82 = ["{Artists}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "single", "{DateAdded}"] +add_song = """ INSERT INTO song(idSong, idAlbum, idPath, strArtistDisp, strGenres, strTitle, iTrack, iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, rating, comment, dateAdded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ +add_song_obj = ["{SongId}", "{AlbumId}", "{PathId}", "{Artists}", "{Genre}", "{Title}", "{Index}", "{Runtime}", "{Year}", "{Filename}", "{UniqueId}", "{PlayCount}", "{DatePlayed}", "{Rating}", "{Comment}", "{DateAdded}"] +add_song82 = """ INSERT INTO song(idSong, idAlbum, idPath, strArtistDisp, strGenres, strTitle, iTrack, iDuration, strReleaseDate, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, rating, comment, dateAdded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ +add_genre = """ INSERT INTO genre(idGenre, strGenre) VALUES (?, ?) """ +add_genres_obj = ["{AlbumId}", "{Genres}", "album"] +disable_rescan = """ INSERT OR REPLACE INTO versiontagscan(idVersion, iNeedsScan) VALUES (?, ?) """ +update_path = """ UPDATE path SET strPath = ? WHERE idPath = ? """ +update_path_obj = ["{Path}", "{PathId}"] +update_role = """ INSERT OR REPLACE INTO role(idRole, strRole) VALUES (?, ?) """ +update_role_obj = [1, "artist"] +update_artist_name = """ UPDATE artist SET strArtist = ? WHERE idArtist = ? """ +update_artist_name_obj = ["{Name}", "{ArtistId}"] +update_artist = """ UPDATE artist SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?, lastScraped = ?, strSortName = ? WHERE idArtist = ? """ +update_artist82 = """ UPDATE artist SET strGenres = ?, strBiography = ?, strImage = ?, lastScraped = ?, strSortName = ?, dateAdded = ? WHERE idArtist = ? """ +update_link = """ INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) VALUES (?, ?, ?) """ +update_link_obj = ["{ArtistId}", "{AlbumId}", "{Name}"] +update_discography = """ INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) VALUES (?, ?, ?) """ +update_discography_obj = ["{ArtistId}", "{Title}", "{Year}"] +update_album = """ UPDATE album SET strArtistDisp = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?, iUserrating = ?, lastScraped = ?, strReleaseType = ? WHERE idAlbum = ? """ +update_album_obj = ["{Artists}", "{Year}", "{Genre}", "{Bio}", "{Thumb}", "{Rating}", "{LastScraped}", "album", "{AlbumId}"] +update_album82 = """ UPDATE album SET strArtistDisp = ?, strReleaseDate = ?, strGenres = ?, strReview = ?, strImage = ?, iUserrating = ?, lastScraped = ?, bScrapedMBID = 1, strReleaseType = ?, dateAdded = ? WHERE idAlbum = ? """ +update_album_obj82 = ["{Artists}", "{Year}", "{Genre}", "{Bio}", "{Thumb}", "{Rating}", "{LastScraped}", "album", "{DateAdded}", "{AlbumId}"] +update_album_artist = """ UPDATE album SET strArtistDisp = ? WHERE idAlbum = ? """ +update_song_obj = ["{AlbumId}", "{Artists}", "{Genre}", "{Title}", "{Index}", "{Runtime}", "{Year}", "{Filename}", "{PlayCount}", "{DatePlayed}", "{Rating}", "{Comment}", "{DateAdded}", "{SongId}"] +update_song = """ UPDATE song SET idAlbum = ?, strArtistDisp = ?, strGenres = ?, strTitle = ?, iTrack = ?, iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?, rating = ?, comment = ?, dateAdded = ? WHERE idSong = ? """ +update_song82 = """ UPDATE song SET idAlbum = ?, strArtistDisp = ?, strGenres = ?, strTitle = ?, iTrack = ?, iDuration = ?, strReleaseDate = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?, rating = ?, comment = ?, dateAdded = ? WHERE idSong = ? """ +update_song_artist = """ INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist) VALUES (?, ?, ?, ?, ?) """ +update_song_artist_obj = ["{ArtistId}", "{SongId}", 1, "{Index}", "{Name}"] +update_song_album = """ INSERT OR REPLACE INTO albuminfosong(idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) VALUES (?, ?, ?, ?, ?) """ +update_song_album_obj = ["{SongId}", "{AlbumId}", "{Index}", "{Title}", "{Runtime}"] +update_song_rating = """ UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ? """ +update_song_rating_obj = ["{PlayCount}", "{DatePlayed}", "{Rating}", "{KodiId}"] +update_genre_album = """ INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) VALUES (?, ?) """ +update_genre_song = """ INSERT OR REPLACE INTO song_genre(idGenre, idSong) VALUES (?, ?) """ +update_genre_song_obj = ["{SongId}", "{Genres}", "song"] +delete_genres_album = """ DELETE FROM album_genre WHERE idAlbum = ? """ +delete_genres_song = """ DELETE FROM song_genre WHERE idSong = ? """ +delete_artist = """ DELETE FROM artist WHERE idArtist = ? """ +delete_album = """ DELETE FROM album WHERE idAlbum = ? """ +delete_song = """ DELETE FROM song WHERE idSong = ? """ +delete_rescan = """ DELETE FROM versiontagscan """ +delete_artwork = """DELETE FROM art WHERE url = ?""" +add_path = """INSERT OR REPLACE INTO path(idPath, strPath) VALUES (?, ?)""" +add_path_obj = ["{Path}"] +get_path = """SELECT idPath FROM path WHERE strPath = ?""" +get_path_obj = ["{Path}"] +create_path = """SELECT coalesce(max(idPath), 0) FROM path""" diff --git a/core/queries_texture.py b/core/queries_texture.py new file mode 100644 index 000000000..711b3fb64 --- /dev/null +++ b/core/queries_texture.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +get_cache = """SELECT cachedurl FROM texture WHERE url = ?""" +delete_cache = """DELETE FROM texture WHERE url = ?""" diff --git a/core/queries_videos.py b/core/queries_videos.py new file mode 100644 index 000000000..d1c439035 --- /dev/null +++ b/core/queries_videos.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +#Queries for the Kodi database. obj reflect key/value to retrieve from emby items. +#Some functions require additional information, therefore obj do not always reflect +#the Kodi database query values. +create_path = """SELECT coalesce(max(idPath), 0) FROM path""" +create_file = """SELECT coalesce(max(idFile), 0) FROM files""" +create_person = """SELECT coalesce(max(actor_id), 0) FROM actor""" +create_genre = """SELECT coalesce(max(genre_id), 0) FROM genre""" +create_studio = """SELECT coalesce(max(studio_id), 0) FROM studio""" +create_bookmark = """SELECT coalesce(max(idBookmark), 0) FROM bookmark""" +create_tag = """SELECT coalesce(max(tag_id), 0) FROM tag""" +create_unique_id = """SELECT coalesce(max(uniqueid_id), 0) FROM uniqueid""" +create_rating = """SELECT coalesce(max(rating_id), 0) FROM rating""" +create_movie = """SELECT coalesce(max(idMovie), 0) FROM movie""" +create_set = """SELECT coalesce(max(idSet), 0) FROM sets""" +create_country = """SELECT coalesce(max(country_id), 0) FROM country""" +create_musicvideo = """SELECT coalesce(max(idMVideo), 0) FROM musicvideo""" +create_tvshow = """SELECT coalesce(max(idShow), 0) FROM tvshow""" +create_season = """ SELECT coalesce(max(idSeason), 0) FROM seasons""" +create_episode = """SELECT coalesce(max(idEpisode), 0) FROM episode""" +get_path = """SELECT idPath FROM path WHERE strPath = ?""" +get_path_obj = ["{Path}"] +get_file = """SELECT idFile FROM files WHERE idPath = ? AND strFilename = ?""" +get_file_obj = ["{FileId}"] +get_filename = """SELECT strFilename FROM files WHERE idFile = ?""" +get_person = """SELECT actor_id FROM actor WHERE name = ? COLLATE NOCASE""" +get_genre = """SELECT genre_id FROM genre WHERE name = ? COLLATE NOCASE""" +get_studio = """SELECT studio_id FROM studio WHERE name = ? COLLATE NOCASE""" +get_tag = """SELECT tag_id FROM tag WHERE name = ? COLLATE NOCASE""" +get_tag_movie_obj = ["Favorite movies", "{MovieId}", "movie"] +get_tag_mvideo_obj = ["Favorite musicvideos", "{MvideoId}", "musicvideo"] +get_tag_episode_obj = ["Favorite tvshows", "{KodiId}", "tvshow"] +get_art = """SELECT url FROM art WHERE media_id = ? AND media_type = ? AND type = ?""" +get_movie = """SELECT * FROM movie WHERE idMovie = ?""" +get_movie_obj = ["{MovieId}"] +get_rating = """SELECT rating_id FROM rating WHERE media_type = ? AND media_id = ? AND rating_type = ? COLLATE NOCASE""" +get_rating_movie_obj = ["movie", "{MovieId}", "{RatingType}"] +get_rating_episode_obj = ["episode", "{EpisodeId}", "default"] +get_rating_tvshow_obj = ["tvshow", "{ShowId}", "default"] +get_unique_id = """SELECT uniqueid_id FROM uniqueid WHERE media_type = ? AND media_id = ?""" +get_unique_id_movie_obj = ["movie", "{MovieId}"] +get_unique_id_tvshow_obj = ["tvshow", "{ShowId}"] +get_unique_id_episode_obj = ["episode", "{EpisodeId}"] +get_country = """SELECT country_id FROM country WHERE name = ? COLLATE NOCASE""" +get_set = """SELECT idSet FROM sets WHERE strSet = ? COLLATE NOCASE""" +get_musicvideo = """SELECT * FROM musicvideo WHERE idMVideo = ?""" +get_musicvideo_obj = ["{MvideoId}"] +get_tvshow = """SELECT * FROM tvshow WHERE idShow = ?""" +get_tvshow_obj = ["{ShowId}"] +get_episode = """SELECT * FROM episode WHERE idEpisode = ?""" +get_episode_obj = ["{EpisodeId}"] +get_season = """SELECT idSeason FROM seasons WHERE idShow = ? AND season = ?""" +get_season_obj = ["{Title}", "{ShowId}", "{Index}"] +get_season_special_obj = [None, "{ShowId}", -1] +get_season_episode_obj = [None, "{ShowId}", "{Season}"] +get_backdrops = """SELECT url FROM art WHERE media_id = ? AND media_type = ? AND type LIKE ?""" +get_art = """SELECT url FROM art WHERE media_id = ? AND media_type = ? AND type = ?""" +get_art_url = """SELECT url, type FROM art WHERE media_id = ? AND media_type = ?""" +get_show_by_unique_id = """SELECT idShow FROM tvshow_view WHERE uniqueid_value = ?""" +get_total_episodes = """SELECT totalCount FROM tvshowcounts WHERE idShow = ?""" +get_total_episodes_obj = ["{ParentId}"] +get_artwork = """SELECT url FROM art WHERE media_type != 'actor'""" +get_settings = """SELECT idFile, Deinterlace, ViewMode, ZoomAmount, PixelRatio, VerticalShift, AudioStream, SubtitleStream, SubtitleDelay, SubtitlesOn, Brightness, Contrast, Gamma, VolumeAmplification, AudioDelay, ResumeTime, Sharpness, NoiseReduction, NonLinStretch, PostProcess, ScalingMethod, StereoMode, StereoInvert, VideoStream, TonemapMethod, TonemapParam, Orientation, CenterMixLevel FROM settings Where idFile = ?""" +add_path = """INSERT OR REPLACE INTO path(idPath, strPath) VALUES (?, ?)""" +add_path_obj = ["{Path}"] +add_file = """INSERT OR REPLACE INTO files(idFile, idPath, strFilename) VALUES (?, ?, ?)""" +add_file_obj = ["{PathId}", "{Filename}"] +add_person = """INSERT OR REPLACE INTO actor(actor_id, name) VALUES (?, ?)""" +add_people_movie_obj = ["{People}", "{MovieId}", "movie"] +add_people_mvideo_obj = ["{People}", "{MvideoId}", "musicvideo"] +add_people_tvshow_obj = ["{People}", "{ShowId}", "tvshow"] +add_people_episode_obj = ["{People}", "{EpisodeId}", "episode"] +add_actor_link = """INSERT OR REPLACE INTO actor_link(actor_id, media_id, media_type, role, cast_order) VALUES (?, ?, ?, ?, ?)""" +add_link = """INSERT OR REPLACE INTO {LinkType}(actor_id, media_id, media_type) VALUES (?, ?, ?)""" +add_genre = """INSERT OR REPLACE INTO genre(genre_id, name) VALUES (?, ?)""" +add_genres_movie_obj = ["{Genres}", "{MovieId}", "movie"] +add_genres_mvideo_obj = ["{Genres}", "{MvideoId}", "musicvideo"] +add_genres_tvshow_obj = ["{Genres}", "{ShowId}", "tvshow"] +add_studio = """INSERT OR REPLACE INTO studio(studio_id, name) VALUES (?, ?)""" +add_studios_movie_obj = ["{Studios}", "{MovieId}", "movie"] +add_studios_mvideo_obj = ["{Studios}", "{MvideoId}", "musicvideo"] +add_studios_tvshow_obj = ["{Studios}", "{ShowId}", "tvshow"] +add_bookmark = """INSERT OR REPLACE INTO bookmark(idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) VALUES (?, ?, ?, ?, ?, ?)""" +add_bookmark_obj = ["{FileId}", "{PlayCount}", "{DatePlayed}", "{Resume}", "{Runtime}", "DVDPlayer", 1] +add_stacktimes_obj = ["{FileId}", "{StackTimes}"] +add_stacktimes = """INSERT OR REPLACE INTO stacktimes(idFile, times) VALUES (?, ?)""" +add_streams_obj = ["{FileId}", "{Streams}", "{Runtime}"] +add_stream_video = """INSERT OR REPLACE INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth, iVideoHeight, iVideoDuration, strStereoMode) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""" +add_stream_video_obj = ["{FileId}", 0, "{codec}", "{aspect}", "{width}", "{height}", "{Runtime}", "{3d}"] +add_stream_audio = """INSERT OR REPLACE INTO streamdetails(idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) VALUES (?, ?, ?, ?, ?)""" +add_stream_audio_obj = ["{FileId}", 1, "{codec}", "{channels}", "{language}"] +add_stream_sub = """INSERT OR REPLACE INTO streamdetails(idFile, iStreamType, strSubtitleLanguage) VALUES (?, ?, ?)""" +add_stream_sub_obj = ["{FileId}", 2, "{language}"] +add_tag = """INSERT OR REPLACE INTO tag(tag_id, name) VALUES (?, ?)""" +add_tags_movie_obj = ["{Tags}", "{MovieId}", "movie"] +add_tags_mvideo_obj = ["{Tags}", "{MvideoId}", "musicvideo"] +add_tags_tvshow_obj = ["{Tags}", "{ShowId}", "tvshow"] +add_art = """INSERT OR REPLACE INTO art(media_id, media_type, type, url) VALUES (?, ?, ?, ?)""" +add_movie = """INSERT OR REPLACE INTO movie(idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, userrating, premiered) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_movie_obj = ["{MovieId}", "{FileId}", "{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{OriginalTitle}", "{Studio}", "{Trailer}", "{Country}", "{CriticRating}", "{Premiered}"] +add_rating = """INSERT OR REPLACE INTO rating(rating_id, media_id, media_type, rating_type, rating, votes) VALUES (?, ?, ?, ?, ?, ?)""" +add_rating_movie_obj = ["{RatingId}", "{MovieId}", "movie", "{RatingType}", "{Rating}", "{Votes}"] +add_rating_tvshow_obj = ["{RatingId}", "{ShowId}", "tvshow", "default", "{Rating}", "{Votes}"] +add_rating_episode_obj = ["{RatingId}", "{EpisodeId}", "episode", "default", "{Rating}", "{Votes}"] +add_unique_id = """INSERT OR REPLACE INTO uniqueid(uniqueid_id, media_id, media_type, value, type) VALUES (?, ?, ?, ?, ?)""" +add_unique_id_movie_obj = ["{Unique}", "{MovieId}", "movie", "{UniqueId}", "{ProviderName}"] +add_unique_id_tvshow_obj = ["{Unique}", "{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}"] +add_unique_id_episode_obj = ["{Unique}", "{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}"] +add_country = """INSERT OR REPLACE INTO country(country_id, name) VALUES (?, ?)""" +add_set = """INSERT OR REPLACE INTO sets(idSet, strSet, strOverview) VALUES (?, ?, ?)""" +add_set_obj = ["{Title}", "{Overview}"] +add_musicvideo = """INSERT OR REPLACE INTO musicvideo(idMVideo,idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12, premiered) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_musicvideo_obj = ["{MvideoId}", "{FileId}", "{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}", "{Plot}", "{Album}", "{Artists}", "{Genre}", "{Index}", "{Premiere}"] +add_tvshow = """INSERT OR REPLACE INTO tvshow(idShow, c00, c01, c02, c04, c05, c08, c09, c10, c12, c13, c14, c15) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_tvshow_obj = ["{ShowId}", "{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{OriginalTitle}", "disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}"] +add_season = """INSERT OR REPLACE INTO seasons(idSeason, idShow, season) VALUES (?, ?, ?)""" +add_episode = """INSERT OR REPLACE INTO episode(idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, idShow, c15, c16, idSeason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_episode_obj = ["{EpisodeId}", "{FileId}", "{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}", "{Directors}", "{Season}", "{Index}", "{OriginalTitle}", "{ShowId}", "{AirsBeforeSeason}", "{AirsBeforeEpisode}", "{SeasonId}"] +add_art = """INSERT OR REPLACE INTO art(media_id, media_type, type, url) VALUES (?, ?, ?, ?)""" +update_path = """UPDATE path SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ? WHERE idPath = ?""" +update_path_movie_obj = ["{Path}", "movies", "metadata.local", 1, "{PathId}"] +update_path_toptvshow_obj = ["{TopLevel}", "tvshows", "metadata.local", 1, "{TopPathId}"] +update_path_tvshow_obj = ["{Path}", None, None, 1, "{PathId}"] +update_path_episode_obj = ["{Path}", None, None, 1, "{PathId}"] +update_path_mvideo_obj = ["{Path}", "musicvideos", None, 1, "{PathId}"] +update_file = """UPDATE files SET idPath = ?, strFilename = ?, dateAdded = ? WHERE idFile = ?""" +update_file_obj = ["{PathId}", "{Filename}", "{DateAdded}", "{FileId}"] +update_genres = """INSERT OR REPLACE INTO genre_link(genre_id, media_id, media_type) VALUES (?, ?, ?)""" +update_studios = """INSERT OR REPLACE INTO studio_link(studio_id, media_id, media_type) VALUES (?, ?, ?)""" +update_playcount = """UPDATE files SET playCount = ?, lastPlayed = ? WHERE idFile = ?""" +update_tag = """INSERT OR REPLACE INTO tag_link(tag_id, media_id, media_type) VALUES (?, ?, ?)""" +update_art = """UPDATE art SET url = ? WHERE media_id = ? AND media_type = ? AND type = ?""" +update_actor = """INSERT OR REPLACE INTO actor_link(actor_id, media_id, media_type, role, cast_order) VALUES (?, ?, ?, ?, ?)""" +update_link = """ INSERT OR REPLACE INTO {LinkType}(actor_id, media_id, media_type) VALUES (?, ?, ?) """ +get_update_link = """SELECT * FROM {LinkType} WHERE actor_id = ? AND media_id = ? AND media_type = ? COLLATE NOCASE""" +update_movie = """UPDATE movie SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?, c16 = ?, c18 = ?, c19 = ?, c21 = ?, userrating = ?, premiered = ? WHERE idMovie = ?""" +update_movie_obj = ["{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{OriginalTitle}", "{Studio}", "{Trailer}", "{Country}", "{CriticRating}", "{Premiered}", "{MovieId}"] +update_rating = """UPDATE rating SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ? WHERE rating_id = ?""" +update_rating_movie_obj = ["{MovieId}", "movie", "{RatingType}", "{Rating}", "{Votes}", "{RatingId}"] +update_rating_tvshow_obj = ["{ShowId}", "tvshow", "default", "{Rating}", "{Votes}", "{RatingId}"] +update_rating_episode_obj = ["{EpisodeId}", "episode", "default", "{Rating}", "{Votes}", "{RatingId}"] +update_unique_id = """UPDATE uniqueid SET media_id = ?, media_type = ?, value = ?, type = ? WHERE uniqueid_id = ?""" +update_unique_id_movie_obj = ["{MovieId}", "movie", "{UniqueId}", "{ProviderName}", "{Unique}"] +update_unique_id_tvshow_obj = ["{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}", "{Unique}"] +update_unique_id_episode_obj = ["{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}", "{Unique}"] +update_country = """INSERT OR REPLACE INTO country_link(country_id, media_id, media_type) VALUES (?, ?, ?)""" +update_country_obj = ["{Countries}", "{MovieId}", "movie"] +update_set = """UPDATE sets SET strSet = ?, strOverview = ? WHERE idSet = ?""" +update_set_obj = ["{Title}", "{Overview}", "{SetId}"] +update_movie_set = """UPDATE movie SET idSet = ? WHERE idMovie = ?""" +update_movie_set_obj = ["{SetId}", "{MovieId}"] +update_musicvideo = """UPDATE musicvideo SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, premiered = ? WHERE idMVideo = ?""" +update_musicvideo_obj = ["{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}", "{Plot}", "{Album}", "{Artists}", "{Genre}", "{Index}", "{Premiere}", "{MvideoId}"] +update_tvshow = """UPDATE tvshow SET c00 = ?, c01 = ?, c02 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?, c10 = ?, c12 = ?, c13 = ?, c14 = ?, c15 = ? WHERE idShow = ?""" +update_tvshow_obj = ["{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{OriginalTitle}", "disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}", "{ShowId}"] +update_tvshow_link = """INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath) VALUES (?, ?)""" +update_tvshow_link_obj = ["{ShowId}", "{PathId}"] +update_season = """UPDATE seasons SET name = ? WHERE idSeason = ?""" +update_episode = """UPDATE episode SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?, c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?, idShow = ? WHERE idEpisode = ?""" +update_episode_obj = ["{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}", "{Directors}", "{Season}", "{Index}", "{OriginalTitle}", "{AirsBeforeSeason}", "{AirsBeforeEpisode}", "{SeasonId}", "{ShowId}", "{EpisodeId}"] +update_settings = """INSERT OR REPLACE INTO settings(idFile, Deinterlace, ViewMode, ZoomAmount, PixelRatio, VerticalShift, AudioStream, SubtitleStream, SubtitleDelay, SubtitlesOn, Brightness, Contrast, Gamma, VolumeAmplification, AudioDelay, ResumeTime, Sharpness, NoiseReduction, NonLinStretch, PostProcess, ScalingMethod, StereoMode, StereoInvert, VideoStream, TonemapMethod, TonemapParam, Orientation, CenterMixLevel) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +update_settings_obj = ["{FileId}", "{Deinterlace}", "{ViewMode}", "{ZoomAmount}", "{PixelRatio}", "{VerticalShift}", "{AudioStream}", "{SubtitleStream}", "{SubtitleDelay}", "{SubtitlesOn}", "{Brightness}", "{Contrast}", "{Gamma}", "{VolumeAmplification}", "{AudioDelay}", 0, "{Sharpness}", "{NoiseReduction}", "{NonLinStretch}", "{PostProcess}", "{ScalingMethod}", "{StereoMode}", 0, -1, 1, 1.0, 0, "{CenterMixLevel}"] +delete_path = """DELETE FROM path WHERE idPath = ?""" +delete_path_obj = ["{PathId}"] +delete_file = """DELETE FROM files WHERE idFile = ?""" +delete_file_obj = ["{Path}", "{Filename}"] +delete_file_by_path = """DELETE FROM files WHERE idPath = ? AND strFileName = ?""" +delete_genres = """DELETE FROM genre_link WHERE media_id = ? AND media_type = ?""" +delete_bookmark = """DELETE FROM bookmark WHERE idFile = ?""" +delete_streams = """DELETE FROM streamdetails WHERE idFile = ?""" +delete_tags = """DELETE FROM tag_link WHERE media_id = ? AND media_type = ?""" +delete_tag = """DELETE FROM tag_link WHERE tag_id = ? AND media_id = ? AND media_type = ?""" +delete_tag_movie_obj = ["Favorite movies", "{MovieId}", "movie"] +delete_tag_mvideo_obj = ["Favorite musicvideos", "{MvideoId}", "musicvideo"] +delete_tag_episode_obj = ["Favorite tvshows", "{KodiId}", "tvshow"] +delete_movie = """DELETE FROM movie WHERE idMovie = ?""" +delete_movie_obj = ["{KodiId}", "{FileId}"] +delete_set = """DELETE FROM sets WHERE idSet = ?""" +delete_set_obj = ["{KodiId}"] +delete_movie_set = """UPDATE movie SET idSet = null WHERE idMovie = ?""" +delete_movie_set_obj = ["{MovieId}"] +delete_musicvideo = """DELETE FROM musicvideo WHERE idMVideo = ?""" +delete_musicvideo_obj = ["{MvideoId}", "{FileId}"] +delete_tvshow = """DELETE FROM tvshow WHERE idShow = ?""" +delete_season = """DELETE FROM seasons WHERE idSeason = ?""" +delete_episode = """DELETE FROM episode WHERE idEpisode = ?""" +delete_backdrops = """DELETE FROM art WHERE media_id = ? AND media_type = ? AND type LIKE ?""" +delete_artwork = """DELETE FROM art WHERE url = ?""" +delete_unique_ids = """DELETE FROM uniqueid WHERE media_id = ? AND media_type = ?""" +delete_unique_ids_movie_obj = ["{MovieId}", "movie"] +delete_unique_ids_tvshow_obj = ["{ShowId}", "tvshow"] +delete_unique_ids_episode_obj = ["{EpisodeId}", "episode"] diff --git a/core/tvshows.py b/core/tvshows.py new file mode 100644 index 000000000..6cd17e659 --- /dev/null +++ b/core/tvshows.py @@ -0,0 +1,700 @@ +# -*- coding: utf-8 -*- +import logging +import sqlite3 +import ntpath + +import emby.downloader +import database.queries +import database.emby_db +import helper.wrapper +import helper.api +from . import obj_ops +from . import common +from . import queries_videos +from . import artwork +from . import kodi + +class TVShows(): + def __init__(self, server, embydb, videodb, direct_path, Utils, update_library=False): + self.LOG = logging.getLogger("EMBY.core.tvshows.TVShows") + self.Utils = Utils + self.update_library = update_library + self.server = server + self.emby = embydb + self.video = videodb + self.direct_path = direct_path + self.emby_db = database.emby_db.EmbyDatabase(embydb.cursor) + self.objects = obj_ops.Objects(self.Utils) + self.item_ids = [] + self.display_multiep = self.Utils.settings('displayMultiEpLabel.bool') + self.Downloader = emby.downloader.Downloader(self.Utils) + self.Common = common.Common(self.emby_db, self.objects, self.Utils, self.direct_path, self.server) + self.KodiDBIO = kodi.Kodi(videodb.cursor) + self.TVShowsDBIO = TVShowsDBIO(videodb.cursor) + self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.Utils) + + def __getitem__(self, key): + if key == 'Series': + return self.tvshow + elif key == 'Season': + return self.season + elif key == 'Episode': + return self.episode + + @helper.wrapper.stop + def tvshow(self, item, library=None, pooling=None, redirect=False): + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + If the show is empty, try to remove it. + Process seasons. + Apply series pooling. + ''' + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Series') + obj['Item'] = item + obj['Library'] = library + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + update = True + + if not self.Utils.settings('syncEmptyShows.bool') and not obj['RecursiveCount']: + self.LOG.info("Skipping empty show %s: %s", obj['Title'], obj['Id']) + TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).remove(obj['Id']) + return False + + if pooling is None: + StackedID = self.emby_db.get_stack(obj['PresentationKey']) or obj['Id'] + + if str(StackedID) != obj['Id']: + return TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).tvshow(obj['Item'], library=obj['Library'], pooling=StackedID) + + try: + obj['ShowId'] = e_item[0] + obj['PathId'] = e_item[2] + except TypeError as error: + update = False + self.LOG.debug("ShowId %s not found", obj['Id']) + obj['ShowId'] = self.TVShowsDBIO.create_entry() + else: + if self.TVShowsDBIO.get(*self.Utils.values(obj, queries_videos.get_tvshow_obj)) is None: + update = False + self.LOG.info("ShowId %s missing from kodi. repairing the entry.", obj['ShowId']) + + obj['Path'] = API.get_file_path(obj['Item']['Path']) + obj['Genres'] = obj['Genres'] or [] + obj['People'] = obj['People'] or [] + obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) + obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Genre'] = " / ".join(obj['Genres']) + obj['People'] = API.get_people_artwork(obj['People']) + obj['Plot'] = API.get_overview(obj['Plot']) + obj['Studio'] = " / ".join(obj['Studios']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + + if obj['Status'] != 'Ended': + obj['Status'] = None + + if not self.get_path_filename(obj): + return False + + if obj['Premiere']: + obj['Premiere'] = str(self.Utils.convert_to_local(obj['Premiere'])).split('.')[0].replace('T', " ") + + tags = [] + tags.extend(obj['TagItems'] or obj['Tags'] or []) + tags.append(obj['LibraryName']) + + if obj['Favorite']: + tags.append('Favorite tvshows') + + obj['Tags'] = tags + + if update: + self.tvshow_update(obj) + else: + self.tvshow_add(obj) + + if pooling: + obj['SeriesId'] = pooling + self.LOG.info("POOL %s [%s/%s]", obj['Title'], obj['Id'], obj['SeriesId']) + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_pool_obj)) + return True + + self.TVShowsDBIO.link(*self.Utils.values(obj, queries_videos.update_tvshow_link_obj)) + self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_tvshow_obj)) + self.KodiDBIO.add_tags(*self.Utils.values(obj, queries_videos.add_tags_tvshow_obj)) + self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_tvshow_obj)) + self.KodiDBIO.add_genres(*self.Utils.values(obj, queries_videos.add_genres_tvshow_obj)) + self.KodiDBIO.add_studios(*self.Utils.values(obj, queries_videos.add_studios_tvshow_obj)) + self.ArtworkDBIO.add(obj['Artwork'], obj['ShowId'], "tvshow") + self.item_ids.append(obj['Id']) + + if "StackTimes" in obj: + self.KodiDBIO.add_stacktimes(*self.Utils.values(obj, queries_videos.add_stacktimes_obj)) + + if redirect: + self.LOG.info("tvshow added as a redirect") + return True + + season_episodes = {} + + try: + all_seasons = self.server['api'].get_seasons(obj['Id'])['Items'] + except Exception as error: + self.LOG.error("Unable to pull seasons for %s", obj['Title']) + self.LOG.error(error) + return True + + for season in all_seasons: + if (self.update_library and season['SeriesId'] != obj['Id']) or (not update and not self.update_library): + season_episodes[season['Id']] = season.get('SeriesId', obj['Id']) + + try: + self.emby_db.get_item_by_id(season['Id'])[0] + self.item_ids.append(season['Id']) + except TypeError: + self.season(season, obj['ShowId'], obj['LibraryId']) + + season_id = self.TVShowsDBIO.get_season(*self.Utils.values(obj, queries_videos.get_season_special_obj)) + self.ArtworkDBIO.add(obj['Artwork'], season_id, "season") + + for season in season_episodes: + for episodes in self.Downloader.get_episode_by_season(season_episodes[season], season): + for episode in episodes['Items']: + self.episode(episode) + + return not update + + #Add object to kodi + def tvshow_add(self, obj): + obj['RatingId'] = self.KodiDBIO.create_entry_rating() + self.KodiDBIO.add_ratings(*self.Utils.values(obj, queries_videos.add_rating_tvshow_obj)) + obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() + self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_tvshow_obj)) + + for provider in obj['UniqueIds'] or {}: + unique_id = obj['UniqueIds'][provider] + provider = provider.lower() + + if provider != 'tvdb': + temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) + self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_tvshow_obj)) + + obj['TopPathId'] = self.KodiDBIO.add_path(obj['TopLevel']) + self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_toptvshow_obj)) + obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.get_path_obj)) + self.TVShowsDBIO.add(*self.Utils.values(obj, queries_videos.add_tvshow_obj)) + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_tvshow_obj)) + self.LOG.info("ADD tvshow [%s/%s/%s] %s: %s", obj['TopPathId'], obj['PathId'], obj['ShowId'], obj['Id'], obj['Title']) + + #Update object to kodi + def tvshow_update(self, obj): + obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(obj, queries_videos.get_rating_tvshow_obj)) + self.KodiDBIO.update_ratings(*self.Utils.values(obj, queries_videos.update_rating_tvshow_obj)) + self.KodiDBIO.remove_unique_ids(*self.Utils.values(obj, queries_videos.delete_unique_ids_tvshow_obj)) + obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() + self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_tvshow_obj)) + + for provider in obj['UniqueIds'] or {}: + unique_id = obj['UniqueIds'][provider] + provider = provider.lower() + + if provider != 'tvdb': + temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) + self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_tvshow_obj)) + + self.TVShowsDBIO.update(*self.Utils.values(obj, queries_videos.update_tvshow_obj)) + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE tvshow [%s/%s] %s: %s", obj['PathId'], obj['ShowId'], obj['Id'], obj['Title']) + + #Get the path and build it into protocol://path + def get_path_filename(self, obj): + if not obj['Path']: + self.LOG.info("Path is missing") + return False + + if self.direct_path: + if '\\' in obj['Path']: + obj['Path'] = "%s\\" % obj['Path'] + obj['TopLevel'] = "%s\\" % ntpath.dirname(ntpath.dirname(obj['Path'])) + else: + obj['Path'] = "%s/" % obj['Path'] + obj['TopLevel'] = "%s/" % ntpath.dirname(ntpath.dirname(obj['Path'])) + + obj['Path'] = self.Utils.StringDecode(obj['Path']) + obj['TopLevel'] = self.Utils.StringDecode(obj['TopLevel']) + + if not self.Utils.validate(obj['Path']): + raise Exception("Failed to validate path. User stopped.") + else: + obj['TopLevel'] = "http://127.0.0.1:57578/tvshows/" + obj['Path'] = "%s%s/" % (obj['TopLevel'], obj['Id']) + + return True + + @helper.wrapper.stop + def season(self, item, show_id=None, library_id=None): + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + If the show is empty, try to remove it. + ''' + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Season') + obj['LibraryId'] = library_id + obj['ShowId'] = show_id + + if obj['ShowId'] is None: + if not self.get_show_id(obj): + return False + + obj['SeasonId'] = self.TVShowsDBIO.get_season(*self.Utils.values(obj, queries_videos.get_season_obj)) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + + if obj['Location'] != 'Virtual': + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_season_obj)) + self.item_ids.append(obj['Id']) + + self.ArtworkDBIO.add(obj['Artwork'], obj['SeasonId'], "season") + self.LOG.info("UPDATE season [%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['Title'] or obj['Index'], obj['Id']) + return True + + @helper.wrapper.stop + def episode(self, item, library=None): + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + Create additional entry for widgets. + This is only required for plugin/episode. + ''' + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'Episode') + obj['Item'] = item + obj['Library'] = library + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + update = True + + if obj['Location'] == 'Virtual': + self.LOG.info("Skipping virtual episode %s: %s", obj['Title'], obj['Id']) + return False + + if obj['SeriesId'] is None: + self.LOG.info("Skipping episode %s with missing SeriesId", obj['Id']) + return False + + StackedID = self.emby_db.get_stack(obj['PresentationKey']) or obj['Id'] + + if str(StackedID) != obj['Id']: + self.LOG.info("Skipping stacked episode %s [%s]", obj['Title'], obj['Id']) + TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).remove(StackedID) + + try: + obj['EpisodeId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] + except TypeError: + update = False + self.LOG.debug("EpisodeId %s not found", obj['Id']) + obj['EpisodeId'] = self.TVShowsDBIO.create_entry_episode() + else: + if self.TVShowsDBIO.get_episode(*self.Utils.values(obj, queries_videos.get_episode_obj)) is None: + update = False + self.LOG.info("EpisodeId %s missing from kodi. repairing the entry.", obj['EpisodeId']) + + obj['Item']['MediaSources'][0] = self.objects.MapMissingData(obj['Item']['MediaSources'][0], 'MediaSources') + obj['MediaSourceID'] = obj['Item']['MediaSources'][0]['Id'] + obj['Runtime'] = obj['Item']['MediaSources'][0]['RunTimeTicks'] + + if obj['Item']['MediaSources'][0]['Path']: + obj['Path'] = obj['Item']['MediaSources'][0]['Path'] + + #don't use 3d movies as default + if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + for DataSource in obj['Item']['MediaSources']: + if not "3d" in self.Utils.StringMod(DataSource['Path']): + DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') + obj['Path'] = DataSource['Path'] + obj['MediaSourceID'] = DataSource['Id'] + obj['Runtime'] = DataSource['RunTimeTicks'] + break + + obj['Path'] = API.get_file_path(obj['Path']) + obj['Index'] = obj['Index'] or -1 + obj['Writers'] = " / ".join(obj['Writers'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Plot'] = API.get_overview(obj['Plot']) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['People'] = API.get_people_artwork(obj['People'] or []) + obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = None if not obj['DatePlayed'] else self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) + obj['Audio'] = API.audio_streams(obj['Audio'] or []) + obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj = self.Common.get_path_filename(obj, "tvshows") + + if obj['Premiere']: + obj['Premiere'] = self.Utils.convert_to_local(obj['Premiere']).split('.')[0].replace('T', " ") + + if obj['Season'] is None: + if obj['AbsoluteNumber']: + obj['Season'] = 1 + obj['Index'] = obj['AbsoluteNumber'] + else: + obj['Season'] = 0 + + if obj['AirsAfterSeason']: + obj['AirsBeforeSeason'] = obj['AirsAfterSeason'] + obj['AirsBeforeEpisode'] = 4096 # Kodi default number for afterseason ordering + + if obj['MultiEpisode'] and self.display_multiep: + obj['Title'] = "| %02d | %s" % (obj['MultiEpisode'], obj['Title']) + + if not self.get_show_id(obj): + self.LOG.info("No series id associated") + return False + + obj['SeasonId'] = self.TVShowsDBIO.get_season(*self.Utils.values(obj, queries_videos.get_season_episode_obj)) + + if update: + self.episode_update(obj) + else: + self.episode_add(obj) + + self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_episode_obj)) + self.KodiDBIO.update_file(*self.Utils.values(obj, queries_videos.update_file_obj)) + self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_episode_obj)) + self.KodiDBIO.add_streams(*self.Utils.values(obj, queries_videos.add_streams_obj)) + self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.ArtworkDBIO.update(obj['Artwork']['Primary'], obj['EpisodeId'], "episode", "thumb") + self.item_ids.append(obj['Id']) + return not update + + #Add object to kodi + def episode_add(self, obj): + obj = self.Common.Streamdata_add(obj, False) + obj['RatingId'] = self.KodiDBIO.create_entry_rating() + self.KodiDBIO.add_ratings(*self.Utils.values(obj, queries_videos.add_rating_episode_obj)) + obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() + self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_episode_obj)) + + for provider in obj['UniqueIds'] or {}: + unique_id = obj['UniqueIds'][provider] + provider = provider.lower() + + if provider != 'tvdb': + temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) + self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_episode_obj)) + + obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.add_path_obj)) + obj['FileId'] = self.KodiDBIO.add_file(*self.Utils.values(obj, queries_videos.add_file_obj)) + + try: + self.TVShowsDBIO.add_episode(*self.Utils.values(obj, queries_videos.add_episode_obj)) + except sqlite3.IntegrityError: + self.LOG.error("IntegrityError for %s", obj) + obj['EpisodeId'] = self.TVShowsDBIO.create_entry_episode() + return self.episode_add(obj) + + self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_episode_obj)) + self.LOG.info("ADD episode [%s/%s/%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['EpisodeId'], obj['FileId'], obj['Id'], obj['Title']) + + #Update object to kodi + def episode_update(self, obj): + obj = self.Common.Streamdata_add(obj, True) + obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(obj, queries_videos.get_rating_episode_obj)) + self.KodiDBIO.update_ratings(*self.Utils.values(obj, queries_videos.update_rating_episode_obj)) + self.KodiDBIO.remove_unique_ids(*self.Utils.values(obj, queries_videos.delete_unique_ids_episode_obj)) + obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() + self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_episode_obj)) + + for provider in obj['UniqueIds'] or {}: + unique_id = obj['UniqueIds'][provider] + provider = provider.lower() + + if provider != 'tvdb': + temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) + self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_episode_obj)) + + self.TVShowsDBIO.update_episode(*self.Utils.values(obj, queries_videos.update_episode_obj)) + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.emby_db.update_parent_id(*self.Utils.values(obj, database.queries.update_parent_episode_obj)) + self.LOG.info("UPDATE episode [%s/%s/%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['EpisodeId'], obj['FileId'], obj['Id'], obj['Title']) + + def get_show_id(self, obj): + if obj.get('ShowId'): + return True + + obj['ShowId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_series_obj)) + if obj['ShowId'] is None: + + try: + TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).tvshow(self.server['api'].get_item(obj['SeriesId']), library=None, redirect=True) + obj['ShowId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_series_obj))[0] + except (TypeError, KeyError): + self.LOG.error("Unable to add series %s", obj['SeriesId']) + return False + else: + obj['ShowId'] = obj['ShowId'][0] + + self.item_ids.append(obj['SeriesId']) + return True + + #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + @helper.wrapper.stop + def userdata(self, item): + e_item = self.emby_db.get_item_by_id(item['Id']) + API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + obj = self.objects.map(item, 'EpisodeUserData') + obj['Item'] = item + + if e_item: + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['Media'] = e_item[4] + else: + return + + if obj['Media'] == "tvshow": + if obj['Favorite']: + self.KodiDBIO.get_tag(*self.Utils.values(obj, queries_videos.get_tag_episode_obj)) + else: + self.KodiDBIO.remove_tag(*self.Utils.values(obj, queries_videos.delete_tag_episode_obj)) + elif obj['Media'] == "episode": + obj = self.Common.Streamdata_add(obj, True) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + + if obj['DatePlayed']: + obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + if obj['DateAdded']: + obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + + self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + + self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA %s [%s/%s] %s: %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'], obj['Title']) + return + + @helper.wrapper.stop + def remove(self, item_id): + ''' Remove showid, fileid, pathid, emby reference. + There's no episodes left, delete show and any possible remaining seasons + ''' + e_item = self.emby_db.get_item_by_id(item_id) + obj = {'Id': item_id} + + try: + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['ParentId'] = e_item[3] + obj['Media'] = e_item[4] + except TypeError: + return + + if obj['Media'] == 'episode': + temp_obj = dict(obj) + self.remove_episode(obj['KodiId'], obj['FileId'], obj['Id']) + season = self.emby_db.get_full_item_by_kodi_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)) + + try: + temp_obj['Id'] = season[0] + temp_obj['ParentId'] = season[1] + except TypeError: + return + + if not self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_episode_obj)): + self.remove_season(obj['ParentId'], obj['Id']) + self.emby_db.remove_item(*self.Utils.values(temp_obj, database.queries.delete_item_obj)) + + temp_obj['Id'] = self.emby_db.get_item_by_kodi_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_tvshow_obj)) + + if not self.TVShowsDBIO.get_total_episodes(*self.Utils.values(temp_obj, queries_videos.get_total_episodes_obj)): + for season in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_season_obj)): + self.remove_season(season[1], obj['Id']) + + self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_season_obj)) + self.remove_tvshow(temp_obj['ParentId'], obj['Id']) + self.emby_db.remove_item(*self.Utils.values(temp_obj, database.queries.delete_item_obj)) + elif obj['Media'] == 'tvshow': + obj['ParentId'] = obj['KodiId'] + + for season in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_season_obj)): + temp_obj = dict(obj) + temp_obj['ParentId'] = season[1] + + for episode in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_episode_obj)): + self.remove_episode(episode[1], episode[2], obj['Id']) + + self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_episode_obj)) + + self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)) + self.remove_tvshow(obj['KodiId'], obj['Id']) + elif obj['Media'] == 'season': + obj['ParentId'] = obj['KodiId'] + + for episode in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_episode_obj)): + self.remove_episode(episode[1], episode[2], obj['Id']) + + self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_episode_obj)) + self.remove_season(obj['KodiId'], obj['Id']) + + if not self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)): + self.remove_tvshow(obj['ParentId'], obj['Id']) + self.emby_db.remove_item_by_kodi_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_tvshow_obj)) + + # Remove any series pooling episodes + for episode in self.emby_db.get_media_by_parent_id(obj['Id']): + self.remove_episode(episode[2], episode[3], obj['Id']) + + self.emby_db.remove_media_by_parent_id(obj['Id']) + self.emby_db.remove_item(*self.Utils.values(obj, database.queries.delete_item_obj)) + + def remove_tvshow(self, kodi_id, item_id): + self.ArtworkDBIO.delete(kodi_id, "tvshow") + self.TVShowsDBIO.delete_tvshow(kodi_id) + self.emby_db.remove_item_by_kodi_id(kodi_id, "tvshow") + self.LOG.info("DELETE tvshow [%s] %s", kodi_id, item_id) + + def remove_season(self, kodi_id, item_id): + self.ArtworkDBIO.delete(kodi_id, "season") + self.TVShowsDBIO.delete_season(kodi_id) + self.emby_db.remove_item_by_kodi_id(kodi_id, "season") + self.LOG.info("DELETE season [%s] %s", kodi_id, item_id) + + def remove_episode(self, kodi_id, file_id, item_id): + self.ArtworkDBIO.delete(kodi_id, "episode") + self.TVShowsDBIO.delete_episode(kodi_id, file_id) + self.emby_db.remove_item(item_id) + self.LOG.info("DELETE episode [%s/%s] %s", file_id, kodi_id, item_id) + + #Get all child elements from tv show emby id + def get_child(self, item_id): + e_item = self.emby_db.get_item_by_id(item_id) + obj = {'Id': item_id} + child = [] + + try: + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['ParentId'] = e_item[3] + obj['Media'] = e_item[4] + except TypeError: + return child + + obj['ParentId'] = obj['KodiId'] + + for season in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_season_obj)): + temp_obj = dict(obj) + temp_obj['ParentId'] = season[1] + child.append(season[0]) + + for episode in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_episode_obj)): + child.append(episode[0]) + + for episode in self.emby_db.get_media_by_parent_id(obj['Id']): + child.append(episode[0]) + + return child + +class TVShowsDBIO(): + def __init__(self, cursor): + self.cursor = cursor + + def create_entry_unique_id(self): + self.cursor.execute(queries_videos.create_unique_id) + return self.cursor.fetchone()[0] + 1 + + def create_entry(self): + self.cursor.execute(queries_videos.create_tvshow) + return self.cursor.fetchone()[0] + 1 + + def create_entry_season(self): + self.cursor.execute(queries_videos.create_season) + return self.cursor.fetchone()[0] + 1 + + def create_entry_episode(self): + self.cursor.execute(queries_videos.create_episode) + return self.cursor.fetchone()[0] + 1 + + def get(self, *args): + try: + self.cursor.execute(queries_videos.get_tvshow, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_episode(self, *args): + try: + self.cursor.execute(queries_videos.get_episode, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_total_episodes(self, *args): + try: + self.cursor.execute(queries_videos.get_total_episodes, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def add_unique_id(self, *args): + self.cursor.execute(queries_videos.add_unique_id, args) + + def add(self, *args): + self.cursor.execute(queries_videos.add_tvshow, args) + + def update(self, *args): + self.cursor.execute(queries_videos.update_tvshow, args) + + def link(self, *args): + self.cursor.execute(queries_videos.update_tvshow_link, args) + + def get_season(self, name, *args): + self.cursor.execute(queries_videos.get_season, args) + + try: + season_id = self.cursor.fetchone()[0] + except TypeError: + season_id = self.add_season(*args) + + if name: + self.cursor.execute(queries_videos.update_season, (name, season_id)) + + return season_id + + def add_season(self, *args): + season_id = self.create_entry_season() + self.cursor.execute(queries_videos.add_season, (season_id,) + args) + return season_id + + def add_episode(self, *args): + self.cursor.execute(queries_videos.add_episode, args) + + def update_episode(self, *args): + self.cursor.execute(queries_videos.update_episode, args) + + def delete_tvshow(self, *args): + self.cursor.execute(queries_videos.delete_tvshow, args) + + def delete_season(self, *args): + self.cursor.execute(queries_videos.delete_season, args) + + def delete_episode(self, kodi_id, file_id): + self.cursor.execute(queries_videos.delete_episode, (kodi_id,)) + self.cursor.execute(queries_videos.delete_file, (file_id,)) diff --git a/libraries/dateutil/test/__init__.py b/database/__init__.py similarity index 100% rename from libraries/dateutil/test/__init__.py rename to database/__init__.py diff --git a/database/database.py b/database/database.py new file mode 100644 index 000000000..8bf689c79 --- /dev/null +++ b/database/database.py @@ -0,0 +1,441 @@ +# -*- coding: utf-8 -*- +import datetime +import logging +import json +import os +import sqlite3 +import xbmc +import xbmcvfs +import xbmcgui + +import helper.translate +import helper.utils +import core.obj_ops +#import emby.views +from . import emby_db + +Utils = helper.utils.Utils() +LOG = logging.getLogger("EMBY.database.database") + +class Database(): + ''' This should be called like a context. + i.e. with Database('emby') as db: + db.cursor + db.conn.commit() + ''' + timeout = 120 + discovered = False + discovered_file = None + + #file: emby, texture, music, video, :memory: or path to file + def __init__(self, fileID=None, commit_close=True): + self.db_file = fileID or "video" + self.commit_close = commit_close + self.objects = core.obj_ops.Objects(Utils) + self.path = None + self.conn = None + self.cursor = None + + #Open the connection and return the Database class. + #This is to allow for the cursor, conn and others to be accessible. + def __enter__(self): + self.path = self._sql(self.db_file) + self.conn = sqlite3.connect(self.path, timeout=self.timeout) + self.cursor = self.conn.cursor() + + if self.db_file in ('video', 'music', 'texture', 'emby'): + self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi + + LOG.debug("--->[ database: %s ] %s", self.db_file, id(self.conn)) + return self + + def _get_database(self, path, silent=False): + path = Utils.translatePath(path) + + if not silent: + if not xbmcvfs.exists(path): + raise Exception("Database: %s missing" % path) + + conn = sqlite3.connect(path) + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + conn.close() + + if not len(tables): + raise Exception("Database: %s malformed?" % path) + + return path + + def _sql(self, DBFile): + databases = self.objects.objects + + if DBFile not in ('video', 'music', 'texture') or databases.get('database_set%s' % DBFile): + return self._get_database(databases[DBFile], True) + + #Read Database from filesystem + folder = Utils.translatePath("special://database/") + dirs, files = xbmcvfs.listdir(folder) + DBIDs = { + 'Textures': "texture", + 'MyMusic': "music", + 'MyVideos': "video" + } + + Version = 0 + + for DBID in DBIDs: + key = DBIDs[DBID] + + if key == DBFile: + for Filename in files: + if (Filename.startswith(DBID) and not Filename.endswith('-wal') and not Filename.endswith('-shm') and not Filename.endswith('db-journal')): + Temp = int(''.join(i for i in Filename if i.isdigit())) + + if Temp > Version: + databases[key] = os.path.join(folder, Filename) + databases['database_set%s' % key] = True + Version = Temp + + Utils.window('kodidbverion.' + DBFile, str(Version)) + return databases[DBFile] + + #Close the connection and cursor + def __exit__(self, exc_type, exc_val, exc_tb): + changes = self.conn.total_changes + + if exc_type is not None: # errors raised + LOG.error("type: %s value: %s", exc_type, exc_val) + + if self.commit_close and changes: + LOG.info("[%s] %s rows updated.", self.db_file, changes) + self.conn.commit() + + LOG.debug("---<[ database: %s ] %s", self.db_file, id(self.conn)) + self.cursor.close() + self.conn.close() + + +#Open the databases to test if the file exists +def test_databases(): + with Database('video'): + with Database('music'): + pass + + with Database('emby') as embydb: + emby_tables(embydb.cursor) + + +#Create the tables for the emby database +def emby_tables(cursor): + cursor.execute("CREATE TABLE IF NOT EXISTS emby(emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER, emby_parent_id TEXT)") + cursor.execute("CREATE TABLE IF NOT EXISTS view(view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)") + cursor.execute("CREATE TABLE IF NOT EXISTS MediaSources(emby_id TEXT, MediaIndex INTEGER, Protocol TEXT, MediaSourceId TEXT, Path TEXT, Type TEXT, Container TEXT, Size INTEGER, Name TEXT, IsRemote TEXT, RunTimeTicks INTEGER, SupportsTranscoding TEXT, SupportsDirectStream TEXT, SupportsDirectPlay TEXT, IsInfiniteStream TEXT, RequiresOpening TEXT, RequiresClosing TEXT, RequiresLooping TEXT, SupportsProbing TEXT, Formats TEXT, Bitrate INTEGER, RequiredHttpHeaders TEXT, ReadAtNativeFramerate TEXT, DefaultAudioStreamIndex INTEGER)") + cursor.execute("CREATE TABLE IF NOT EXISTS VideoStreams(emby_id TEXT, MediaIndex INTEGER, VideoIndex INTEGER, Codec TEXT, TimeBase TEXT, CodecTimeBase TEXT, VideoRange TEXT, DisplayTitle TEXT, IsInterlaced TEXT, BitRate INTEGER, BitDepth INTEGER, RefFrames INTEGER, IsDefault TEXT, IsForced TEXT, Height INTEGER, Width INTEGER, AverageFrameRate INTEGER, RealFrameRate INTEGER, Profile TEXT, Type TEXT, AspectRatio TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, PixelFormat TEXT, Level INTEGER, IsAnamorphic TEXT, StreamIndex INTEGER)") + cursor.execute("CREATE TABLE IF NOT EXISTS AudioStreams(emby_id TEXT, MediaIndex INTEGER, AudioIndex INTEGER, Codec TEXT, Language TEXT, TimeBase TEXT, CodecTimeBase TEXT, DisplayTitle TEXT, DisplayLanguage TEXT, IsInterlaced TEXT, ChannelLayout TEXT, BitRate INTEGER, Channels INTEGER, SampleRate INTEGER, IsDefault TEXT, IsForced TEXT, Profile TEXT, Type TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, StreamIndex INTEGER)") + cursor.execute("CREATE TABLE IF NOT EXISTS Subtitle(emby_id TEXT, MediaIndex INTEGER, SubtitleIndex INTEGER, Codec TEXT, Language TEXT, TimeBase TEXT, CodecTimeBase TEXT, DisplayTitle TEXT, DisplayLanguage TEXT, IsInterlaced TEXT, IsDefault TEXT, IsForced TEXT, Path TEXT, Type TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, StreamIndex INTEGER)") + + columns = cursor.execute("SELECT * FROM VideoStreams") + descriptions = [description[0] for description in columns.description] + + if 'StreamIndex' not in descriptions: + LOG.info("Add missing column VideoStreams -> StreamIndex") + cursor.execute("ALTER TABLE VideoStreams ADD COLUMN StreamIndex 'INTEGER'") + + columns = cursor.execute("SELECT * FROM AudioStreams") + descriptions = [description[0] for description in columns.description] + + if 'StreamIndex' not in descriptions: + LOG.info("Add missing column AudioStreams -> StreamIndex") + cursor.execute("ALTER TABLE AudioStreams ADD COLUMN StreamIndex 'INTEGER'") + + columns = cursor.execute("SELECT * FROM Subtitle") + descriptions = [description[0] for description in columns.description] + + if 'StreamIndex' not in descriptions: + LOG.info("Add missing column Subtitle -> StreamIndex") + cursor.execute("ALTER TABLE Subtitle ADD COLUMN StreamIndex 'INTEGER'") + + columns = cursor.execute("SELECT * FROM emby") + descriptions = [description[0] for description in columns.description] + + if 'emby_parent_id' not in descriptions: + LOG.info("Add missing column emby_parent_id") + cursor.execute("ALTER TABLE emby ADD COLUMN emby_parent_id 'TEXT'") + + if 'presentation_key' not in descriptions: + LOG.info("Add missing column presentation_key") + cursor.execute("ALTER TABLE emby ADD COLUMN presentation_key 'TEXT'") + +#Reset both the emby database and the kodi database. +def reset(Force=False): +# views = emby.views.Views(Utils) + + if not Force: + if not Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33074)): + return + + Utils.window('emby_should_stop.bool', True) + count = 60 + + while Utils.window('emby_sync.bool'): + LOG.info("Sync is running...") + count -= 1 + + if not count: + Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33085)) + return + + if xbmc.Monitor().waitForAbort(1): + return + + reset_kodi() + reset_emby() +# views.delete_playlists() +# views.delete_nodes() + +# if Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33086)): + reset_artwork() + + addon_data = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33087)): + xbmcvfs.delete(os.path.join(addon_data, "settings.xml")) + xbmcvfs.delete(os.path.join(addon_data, "data.json")) + LOG.info("[ reset settings ]") + + if xbmcvfs.exists(os.path.join(addon_data, "sync.json")): + xbmcvfs.delete(os.path.join(addon_data, "sync.json")) + + Utils.settings('enableMusic.bool', False) + Utils.settings('MinimumSetup', "") + Utils.settings('MusicRescan.bool', False) + Utils.settings('SyncInstallRunDone.bool', False) + Utils.settings('Migrate.bool', True) + Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33088)) + xbmc.executebuiltin('RestartApp') + +def reset_kodi(): + Progress = xbmcgui.DialogProgressBG() + Progress.create(helper.translate._('addon_name'), "Delete Kodi-Video Database") + + with Database() as videodb: + videodb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + tables = videodb.cursor.fetchall() + Counter = 0 + Increment = 100.0 / (len(tables) - 1) + + for table in tables: + name = table[0] + + if name != 'version': + Counter += 1 + Progress.update(int(Counter * Increment), message="Delete Kodi-Video Database: " + name) + videodb.cursor.execute("DELETE FROM " + name) + + Progress.close() + + with Database('music') as musicdb: + musicdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + Progress = xbmcgui.DialogProgressBG() + Progress.create(helper.translate._('addon_name'), "Delete Kodi-Music Database") + tables = musicdb.cursor.fetchall() + Counter = 0 + Increment = 100.0 / (len(tables) - 1) + + for table in tables: + name = table[0] + + if name != 'version': + Counter += 1 + Progress.update(int(Counter * Increment), message="Delete Kodi-Music Database: " + name) + musicdb.cursor.execute("DELETE FROM " + name) + + Progress.close() + + LOG.warning("[ reset kodi ]") + +def reset_emby(): + Progress = xbmcgui.DialogProgressBG() + Progress.create(helper.translate._('addon_name'), "Delete Emby Database") + + with Database('emby') as embydb: + embydb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + tables = embydb.cursor.fetchall() + Counter = 0 + Increment = 100.0 / (len(tables) - 2) + + for table in tables: + name = table[0] + + if name not in ('emby', 'view'): + Counter += 1 + Progress.update(int(Counter * Increment), message="Delete Emby Database: " + name) + embydb.cursor.execute("DELETE FROM " + name) + + embydb.cursor.execute("DROP table IF EXISTS emby") + embydb.cursor.execute("DROP table IF EXISTS view") + + Progress.close() + + LOG.warning("[ reset emby ]") + +#Remove all existing texture +def reset_artwork(): + thumbnails = Utils.translatePath('special://thumbnails/') + + if xbmcvfs.exists(thumbnails): + dirs, ignore = xbmcvfs.listdir(thumbnails) + + for directory in dirs: + ignore, thumbs = xbmcvfs.listdir(os.path.join(thumbnails, directory)) + Progress = xbmcgui.DialogProgressBG() + Progress.create(helper.translate._('addon_name'), "Delete Artwork Files: " + directory) + Counter = 0 + ThumbsLen = len(thumbs) + Increment = 0.0 + + if ThumbsLen > 0: + Increment = 100.0 / ThumbsLen + + for thumb in thumbs: + Counter += 1 + Progress.update(int(Counter * Increment), message="Delete Artwork Files: " + directory + " / " + thumb) + LOG.debug("DELETE thumbnail %s", thumb) + xbmcvfs.delete(os.path.join(thumbnails, directory, thumb)) + + Progress.close() + + Progress = xbmcgui.DialogProgressBG() + Progress.create(helper.translate._('addon_name'), "Delete Texture Database") + + with Database('texture') as texdb: + texdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + tables = texdb.cursor.fetchall() + Counter = 0 + Increment = 100.0 / (len(tables) - 1) + + for table in tables: + name = table[0] + + if name != 'version': + Counter += 1 + Progress.update(int(Counter * Increment), message="Delete Texture Database: " + name) + texdb.cursor.execute("DELETE FROM " + name) + + Progress.close() + + LOG.warning("[ reset artwork ]") + +def get_sync(): + path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + try: + with open(os.path.join(path, 'sync.json'), 'rb') as infile: + sync = json.load(infile, encoding='utf-8') + except Exception: + sync = {} + + sync['Libraries'] = sync.get('Libraries', []) + sync['RestorePoint'] = sync.get('RestorePoint', {}) + sync['Whitelist'] = list(set(sync.get('Whitelist', []))) + sync['SortedViews'] = sync.get('SortedViews', []) + return sync + +def save_sync(sync): + path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + sync['Date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + + with open(os.path.join(path, 'sync.json'), 'wb') as outfile: + data = json.dumps(sync, sort_keys=True, indent=4, ensure_ascii=False) + outfile.write(data.encode('utf-8')) + +def get_credentials(): + path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + try: + with open(os.path.join(path, 'data.json'), 'rb') as infile: + credentials = json.load(infile, encoding='utf8') + except Exception: + try: + with open(os.path.join(path, 'data.txt'), 'rb') as infile: + credentials = json.load(infile, encoding='utf-8') + save_credentials(credentials) + + xbmcvfs.delete(os.path.join(path, 'data.txt')) + except Exception: + credentials = {} + + credentials['Servers'] = credentials.get('Servers', []) + return credentials + +def save_credentials(credentials): + credentials = credentials or {} + path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + credentials = json.dumps(credentials, sort_keys=True, indent=4, ensure_ascii=False) + + with open(os.path.join(path, 'data.json'), 'wb') as outfile: + outfile.write(credentials.encode('utf-8')) + +#Get Kodi ID from emby ID +def get_kodiID(emby_id): + with Database('emby') as embydb: + item = emby_db.EmbyDatabase(embydb.cursor).get_item_by_wild_id(emby_id) + + if not item: + LOG.debug("Not an kodi item") + return + + return item + +#Get emby item based on kodi id and media +def get_item(kodi_id, media): + with Database('emby') as embydb: + item = emby_db.EmbyDatabase(embydb.cursor).get_full_item_by_kodi_id(kodi_id, media) + + if not item: + LOG.debug("Not an emby item") + return + + return item + +def get_item_complete(kodi_id, media): + with Database('emby') as embydb: + item = emby_db.EmbyDatabase(embydb.cursor).get_full_item_by_kodi_id_complete(kodi_id, media) + + if not item: + LOG.debug("Not an emby item") + return + + return item + +def get_Presentationkey(EmbyID): + with Database('emby') as embydb: + item = emby_db.EmbyDatabase(embydb.cursor).get_kodiid(EmbyID) + + if not item: + LOG.debug("Not an emby item") + return None + + return item[1] + +#Get emby item based on kodi id and media +def get_ItemsByPresentationkey(PresentationKey): + with Database('emby') as embydb: + items = emby_db.EmbyDatabase(embydb.cursor).get_ItemsByPresentation_key(PresentationKey) + + return items diff --git a/database/emby_db.py b/database/emby_db.py new file mode 100644 index 000000000..a877e043f --- /dev/null +++ b/database/emby_db.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +from . import queries + +class EmbyDatabase(): + def __init__(self, cursor): + self.cursor = cursor + + def get_item_by_id(self, *args): + self.cursor.execute(queries.get_item, args) + return self.cursor.fetchone() + + def add_reference(self, *args): + self.cursor.execute(queries.add_reference, args) + + def add_mediasource(self, *args): + self.cursor.execute(queries.add_mediasource, args) + + def add_videostreams(self, *args): + self.cursor.execute(queries.add_videostreams, args) + + def add_audiostreams(self, *args): + self.cursor.execute(queries.add_audiostreams, args) + + def add_subtitles(self, *args): + self.cursor.execute(queries.add_subtitles, args) + + def update_reference(self, *args): + self.cursor.execute(queries.update_reference, args) + + #Parent_id is the parent Kodi id + def update_parent_id(self, *args): + self.cursor.execute(queries.update_parent, args) + + def get_item_id_by_parent_id(self, *args): + self.cursor.execute(queries.get_item_id_by_parent, args) + return self.cursor.fetchall() + + def get_item_by_parent_id(self, *args): + self.cursor.execute(queries.get_item_by_parent, args) + return self.cursor.fetchall() + + def get_item_by_media_folder(self, *args): + self.cursor.execute(queries.get_item_by_media_folder, args) + return self.cursor.fetchall() + + def get_item_by_wild_id(self, item_id): + self.cursor.execute(queries.get_item_by_wild, (item_id + "%",)) + return self.cursor.fetchall() + + def get_checksum(self, *args): + self.cursor.execute(queries.get_checksum, args) + return self.cursor.fetchall() + + def get_item_by_kodi_id(self, *args): + try: + self.cursor.execute(queries.get_item_by_kodi, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_full_item_by_kodi_id(self, *args): + try: + self.cursor.execute(queries.get_item_by_kodi, args) + return self.cursor.fetchone() + except TypeError: + return + + def get_full_item_by_kodi_id_complete(self, *args): + try: + self.cursor.execute(queries.get_item_by_kodi_complete, args) + return self.cursor.fetchone() + except TypeError: + return + + def get_media_by_id(self, *args): + try: + self.cursor.execute(queries.get_media_by_id, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_media_by_parent_id(self, *args): + self.cursor.execute(queries.get_media_by_parent_id, args) + return self.cursor.fetchall() + + def get_videostreams(self, *args): + self.cursor.execute(queries.get_videostreams, args) + return self.cursor.fetchall() + + def get_mediasourceid(self, *args): + self.cursor.execute(queries.get_mediasourceid, args) + return self.cursor.fetchall() + + def get_mediasource(self, *args): + self.cursor.execute(queries.get_mediasource, args) + return self.cursor.fetchall() + + def get_kodiid(self, *args): + self.cursor.execute(queries.get_kodiid, args) + return self.cursor.fetchone() + + def get_kodifileid(self, *args): + self.cursor.execute(queries.get_kodifileid, args) + return self.cursor.fetchone()[0] + + def get_AudioStreams(self, *args): + self.cursor.execute(queries.get_AudioStreams, args) + return self.cursor.fetchall() + + def get_Subtitles(self, *args): + self.cursor.execute(queries.get_Subtitles, args) + return self.cursor.fetchall() + + def get_embyid_by_kodiid(self, *args): + self.cursor.execute(queries.get_embyid_by_kodiid, args) + return self.cursor.fetchone()[0] + + def remove_item(self, *args): + self.cursor.execute(queries.delete_item, args) + self.cursor.execute(queries.delete_mediasources, args) + self.cursor.execute(queries.delete_videostreams, args) + self.cursor.execute(queries.delete_audiostreams, args) + self.cursor.execute(queries.delete_subtitles, args) + + def remove_item_streaminfos(self, *args): + self.cursor.execute(queries.delete_mediasources, args) + self.cursor.execute(queries.delete_videostreams, args) + self.cursor.execute(queries.delete_audiostreams, args) + self.cursor.execute(queries.delete_subtitles, args) + + def remove_items_by_parent_id(self, *args): + self.cursor.execute(queries.delete_item_by_parent, args) + + def remove_item_by_kodi_id(self, *args): + self.cursor.execute(queries.delete_item_by_kodi, args) + + def remove_wild_item(self, item_id): + self.cursor.execute(queries.delete_item_by_wild, (item_id + "%",)) + + def get_view_name(self, item_id): + try: + self.cursor.execute(queries.get_view_name, (item_id,)) + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_view(self, *args): + try: + self.cursor.execute(queries.get_view, args) + return self.cursor.fetchone() + except TypeError: + return + + def add_view(self, *args): + try: + self.cursor.execute(queries.add_view, args) + except: + return + + def remove_view(self, *args): + self.cursor.execute(queries.delete_view, args) + + def get_views(self, *args): + try: + self.cursor.execute(queries.get_views, args) + return self.cursor.fetchall() + except: + return + + def get_views_by_media(self, *args): + self.cursor.execute(queries.get_views_by_media, args) + return self.cursor.fetchall() + + def get_items_by_media(self, *args): + self.cursor.execute(queries.get_items_by_media, args) + return self.cursor.fetchall() + + def remove_media_by_parent_id(self, *args): + self.cursor.execute(queries.delete_media_by_parent_id, args) + + def get_stack(self, *args): + try: + self.cursor.execute(queries.get_presentation_key, args) + return self.cursor.fetchone()[0] + except: + return + + def get_ItemsByPresentation_key(self, PresentationKey): + self.cursor.execute(queries.get_presentation_key, (PresentationKey + "%",)) + return self.cursor.fetchall() + + def get_version(self, version=None): + if version is not None: + self.cursor.execute(queries.delete_version) + self.cursor.execute(queries.add_version, (version,)) + else: + try: + self.cursor.execute(queries.get_version) + version = self.cursor.fetchone()[0] + except: + pass + + return version diff --git a/database/library.py b/database/library.py new file mode 100644 index 000000000..61b40a0c2 --- /dev/null +++ b/database/library.py @@ -0,0 +1,832 @@ +# -*- coding: utf-8 -*- +import logging + +try: + import queue as Queue +except ImportError: + import Queue + +import threading +from datetime import datetime, timedelta +import xbmc +import xbmcgui + +import core.movies +import core.musicvideos +import core.tvshows +import core.music +import emby.main +import emby.downloader +import emby.views +import helper.translate +import helper.exceptions +from . import database +from . import sync + +class Library(threading.Thread): + def __init__(self, Utils): + self.LOG = logging.getLogger("EMBY.library.Library") + self.started = False + self.stop_thread = False + self.suspend = False + self.pending_refresh = False + self.screensaver = None + self.progress_updates = None + self.total_updates = 0 + self.Utils = Utils + self.direct_path = self.Utils.settings('useDirectPaths') == "1" + self.LIMIT = min(int(self.Utils.settings('limitIndex') or 50), 50) + self.DTHREADS = int(self.Utils.settings('limitThreads') or 3) + self.MEDIA = {'Movie': core.movies.Movies, 'BoxSet': core.movies.Movies, 'MusicVideo': core.musicvideos.MusicVideos, 'TVShow': core.tvshows.TVShows, 'Series': core.tvshows.TVShows, 'Season': core.tvshows.TVShows, 'Episode': core.tvshows.TVShows, 'Music': core.music.Music, 'MusicAlbum': core.music.Music, 'MusicArtist': core.music.Music, 'AlbumArtist': core.music.Music, 'Audio': core.music.Music, 'MusicDisableScan': core.music.MusicDBIO} + self.Downloader = emby.downloader.Downloader(self.Utils) + self.server = emby.main.Emby().get_client() + self.updated_queue = Queue.Queue() + self.userdata_queue = Queue.Queue() + self.removed_queue = Queue.Queue() + self.updated_output = self.NewQueues() + self.userdata_output = self.NewQueues() + self.removed_output = self.NewQueues() + self.notify_output = Queue.Queue() + self.add_lib_queue = Queue.Queue() + self.remove_lib_queue = Queue.Queue() + self.verify_queue = Queue.Queue() + self.emby_threads = [] + self.download_threads = [] + self.notify_threads = [] + self.writer_threads = {'updated': [], 'userdata': [], 'removed': []} + self.database_lock = threading.Lock() + self.music_database_lock = threading.Lock() + self.sync = sync.Sync + self.progress_percent = 0 + threading.Thread.__init__(self) + self.start() + + def NewQueues(self): + return { + 'Movie': Queue.Queue(), + 'BoxSet': Queue.Queue(), + 'MusicVideo': Queue.Queue(), + 'Series': Queue.Queue(), + 'Season': Queue.Queue(), + 'Episode': Queue.Queue(), + 'MusicAlbum': Queue.Queue(), + 'MusicArtist': Queue.Queue(), + 'AlbumArtist': Queue.Queue(), + 'Audio': Queue.Queue() + } + + def get_naming(self, item): + if item['Type'] == 'Episode': + if 'SeriesName' in item: + return "%s: %s" % (item['SeriesName'], item['Name']) + elif item['Type'] == 'Season': + if 'SeriesName' in item: + return "%s: %s" % (item['SeriesName'], item['Name']) + elif item['Type'] == 'MusicAlbum': + if 'AlbumArtist' in item: + return "%s: %s" % (item['AlbumArtist'], item['Name']) + elif item['Type'] == 'Audio': + if item.get('Artists'): + return "%s: %s" % (item['Artists'][0], item['Name']) + + return item['Name'] + + def set_progress_dialog(self): + queue_size = self.worker_queue_size() + + try: + self.progress_percent = int((float(self.total_updates - queue_size) / float(self.total_updates))*100) + except Exception: + self.progress_percent = 0 + + self.LOG.debug("--[ pdialog (%s/%s) ]", queue_size, self.total_updates) + + if self.total_updates < int(self.Utils.settings('syncProgress') or 50): + return + + if self.progress_updates is None: + self.LOG.info("-->[ pdialog ]") + self.progress_updates = xbmcgui.DialogProgressBG() + self.progress_updates.create(helper.translate._('addon_name'), helper.translate._(33178)) + + def update_progress_dialog(self, item): + if self.progress_updates: + message = self.get_naming(item) + self.progress_updates.update(self.progress_percent, message="%s: %s" % (helper.translate._(33178), message)) + + def run(self): + self.LOG.warning("--->[ library ]") + + while not self.stop_thread: + if xbmc.Monitor().waitForAbort(1): + break + + try: + if not self.started and not self.startup(): + self.stop_client() + + if self.sync.running: + continue + + self.service() + + except helper.exceptions.LibraryException as error: + if error.status == 'StopWriteCalled': + continue + + break + + except Exception as error: + self.LOG.exception(error) + break + + if xbmc.Monitor().waitForAbort(2): + break + + self.Utils.window('emby_sync', clear=True) + self.LOG.warning("---<[ library ]") + + @helper.wrapper.stop + def service(self): + ''' If error is encountered, it will rerun this function. + Start new "daemon threads" to process library updates. + (actual daemon thread is not supported in Kodi) + ''' + for thread in self.download_threads: + if thread.Done: + self.removed(thread.removed) + self.download_threads.remove(thread) + + for threads in (self.emby_threads, self.writer_threads['updated'], self.writer_threads['userdata'], self.writer_threads['removed']): + for thread in threads: + if thread.Done: + threads.remove(thread) + + if not xbmc.Player().isPlayingVideo() or self.Utils.settings('syncDuringPlay.bool') or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + if not xbmc.Player().isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + if not self.Utils.window('emby_sync_skip_resume.bool'): + if database.get_sync()['Libraries']: + self.sync_libraries(True) + + self.worker_remove_lib() + self.worker_add_lib() + + self.worker_verify() + self.worker_downloads() + self.worker_sort() + self.worker_updates() + self.worker_userdata() + self.worker_remove() + self.worker_notify() + + if self.pending_refresh: + self.Utils.window('emby_sync.bool', True) + self.set_progress_dialog() + + if not self.Utils.settings('dbSyncScreensaver.bool') and self.screensaver is None: + xbmc.executebuiltin('InhibitIdleShutdown(true)') + self.screensaver = self.Utils.get_screensaver() + self.Utils.set_screensaver(value="") + + if (self.pending_refresh and not self.download_threads and not self.writer_threads['updated'] and not self.writer_threads['userdata'] and not self.writer_threads['removed']): + self.pending_refresh = False + self.save_last_sync() + self.total_updates = 0 + self.Utils.window('emby_sync', clear=True) + + if self.progress_updates: + self.LOG.info("--<[ pdialog ]") + self.progress_updates.close() + self.progress_updates = None + + if not self.Utils.settings('dbSyncScreensaver.bool') and self.screensaver is not None: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + self.Utils.set_screensaver(value=self.screensaver) + self.screensaver = None + + if not xbmc.getCondVisibility('Window.IsMedia'): + xbmc.executebuiltin('UpdateLibrary(video)') + + def stop_client(self): + self.stop_thread = True + + #When there's an active thread. Let the main thread know + def enable_pending_refresh(self): + self.pending_refresh = True + self.Utils.window('emby_sync.bool', True) + + #Get how many items are queued up for worker threads + def worker_queue_size(self): + total = 0 + total += self._worker_update_size() + total += self._worker_userdata_size() + total += self._worker_removed_size() + return total + + def _worker_update_size(self): + total = 0 + + for queues in self.updated_output: + total += self.updated_output[queues].qsize() + + return total + + def _worker_userdata_size(self): + total = 0 + + for queues in self.userdata_output: + total += self.userdata_output[queues].qsize() + + return total + + def _worker_removed_size(self): + total = 0 + total += self.removed_queue.qsize() + + for queues in self.removed_output: + total += self.removed_output[queues].qsize() + + return total + + #Wait 60 seconds to verify the item by moving it to the updated queue to + #verify item is still available to user. + #Used for internal deletion--callback takes too long + #Used for parental control--server does not send a new event when item has been blocked. + def worker_verify(self): + if self.verify_queue.qsize(): + ready = [] + not_ready = [] + + while True: + try: + time_set, item_id = self.verify_queue.get(timeout=1) + except Queue.Empty: + break + + if time_set <= datetime.today(): + ready.append(item_id) + elif item_id not in list(self.removed_queue.queue): + not_ready.append((time_set, item_id,)) + + self.verify_queue.task_done() + + self.updated(ready) + list(map(self.verify_queue.put, not_ready)) # re-add items that are not ready yet + + #Get items from emby and place them in the appropriate queues + def worker_downloads(self): + for queue in ((self.updated_queue, self.updated_output), (self.userdata_queue, self.userdata_output)): + if queue[0].qsize() and len(self.download_threads) < self.DTHREADS: + new_thread = emby.downloader.GetItemWorker(self.server, queue[0], queue[1], self.Utils) + self.LOG.info("-->[ q:download/%s ]", id(new_thread)) + self.download_threads.append(new_thread) + + #Get items based on the local emby database and place item in appropriate queues + def worker_sort(self): + if self.removed_queue.qsize() and len(self.emby_threads) < 2: + new_thread = SortWorker(self) + self.LOG.info("-->[ q:sort/%s ]", id(new_thread)) + self.emby_threads.append(new_thread) + + #Update items in the Kodi database + def worker_updates(self): + if self._worker_removed_size(): + self.LOG.info("[ DELAY UPDATES ]") + return + + for queues in self.updated_output: + queue = self.updated_output[queues] + + if queue.qsize() and not len(self.writer_threads['updated']): + if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): + new_thread = UpdatedWorker(queue, self, "music", self.music_database_lock) + else: + new_thread = UpdatedWorker(queue, self, "video", self.database_lock) + + self.LOG.info("-->[ q:updated/%s/%s ]", queues, id(new_thread)) + self.writer_threads['updated'].append(new_thread) + self.enable_pending_refresh() + + #Update userdata in the Kodi database + def worker_userdata(self): + if self._worker_removed_size(): + self.LOG.info("[ DELAY UPDATES ]") + return + + for queues in self.userdata_output: + queue = self.userdata_output[queues] + + if queue.qsize() and not len(self.writer_threads['userdata']): + if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): + new_thread = UserDataWorker(queue, self, "music", self.music_database_lock) + else: + new_thread = UserDataWorker(queue, self, "video", self.database_lock) + + self.LOG.info("-->[ q:userdata/%s/%s ]", queues, id(new_thread)) + self.writer_threads['userdata'].append(new_thread) + self.enable_pending_refresh() + + #Remove items from the Kodi database + def worker_remove(self): + for queues in self.removed_output: + queue = self.removed_output[queues] + + if queue.qsize() and not len(self.writer_threads['removed']): + if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): + new_thread = RemovedWorker(queue, self, "music", self.music_database_lock) + else: + new_thread = RemovedWorker(queue, self, "video", self.database_lock) + + self.LOG.info("-->[ q:removed/%s/%s ]", queues, id(new_thread)) + self.writer_threads['removed'].append(new_thread) + self.enable_pending_refresh() + + #Notify the user of new additions + def worker_notify(self): + if self.notify_output.qsize() and not len(self.notify_threads): + new_thread = NotifyWorker(self) + self.LOG.info("-->[ q:notify/%s ]", id(new_thread)) + self.notify_threads.append(new_thread) + + def worker_remove_lib(self): + if self.remove_lib_queue.qsize(): + while True: + try: + library_id = self.remove_lib_queue.get(timeout=0.5) + except Queue.Empty: + break + + self._remove_libraries(library_id) + self.remove_lib_queue.task_done() + xbmc.executebuiltin("Container.Refresh") + + def worker_add_lib(self): + if self.add_lib_queue.qsize(): + while True: + try: + library_id, update = self.add_lib_queue.get(timeout=0.5) + except Queue.Empty: + break + + self._add_libraries(library_id, update) + self.add_lib_queue.task_done() + + xbmc.executebuiltin("Container.Refresh") + + def sync_libraries(self, forced=False): + try: + with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: + syncObj.libraries(forced=forced) + except helper.exceptions.LibraryException as error: + raise + + emby.views.Views(self.Utils).get_nodes() + + def _add_libraries(self, library_id, update=False): + try: + with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: + syncObj.libraries(library_id, update) + except helper.exceptions.LibraryException as error: + raise + + emby.views.Views(self.Utils).get_nodes() + + def _remove_libraries(self, library_id): + try: + with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: + syncObj.remove_library(library_id) + + except helper.exceptions.LibraryException as error: + raise + + ViewsClass = emby.views.Views(self.Utils) + ViewsClass.remove_library(library_id) + ViewsClass.get_views() + ViewsClass.get_nodes() + + #Run at startup. + #Check databases. + #Check for the server plugin. + def startup(self): + self.started = True + ViewsClass = emby.views.Views(self.Utils) + ViewsClass.get_views() + ViewsClass.get_nodes() + + try: + if database.get_sync()['Libraries']: + self.sync_libraries() + elif not self.Utils.settings('SyncInstallRunDone.bool'): + with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: + syncObj.libraries() + + ViewsClass.get_nodes() + xbmc.executebuiltin('ReloadSkin()') + return True + + self.get_fast_sync() + return True + except helper.exceptions.LibraryException as error: + self.LOG.error(error.status) + + if error.status in 'SyncLibraryLater': + self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33129)) + self.Utils.settings('SyncInstallRunDone.bool', True) + syncLibs = database.get_sync() + syncLibs['Libraries'] = [] + database.save_sync(syncLibs) + return True + + if error.status == 'CompanionMissing': + self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33099)) + self.Utils.settings('kodiCompanion.bool', False) + return True + + raise + except Exception as error: + self.LOG.exception(error) + + return False + + def get_fast_sync(self): + enable_fast_sync = False + + if self.Utils.settings('SyncInstallRunDone.bool'): + if self.Utils.settings('kodiCompanion.bool'): + for plugin in self.server['api'].get_plugins(): + if plugin['Name'] in ("Emby.Kodi Sync Queue", "Kodi companion"): + enable_fast_sync = True + break + + self.fast_sync(enable_fast_sync) + self.LOG.info("--<[ retrieve changes ]") + + #Movie and userdata not provided by server yet + def fast_sync(self, plugin): + last_sync = self.Utils.settings('LastIncrementalSync') + syncOBJ = database.get_sync() + self.LOG.info("--[ retrieve changes ] %s", last_sync) + + for library in syncOBJ['Whitelist']: + for data in self.Downloader.get_items(library.replace('Mixed:', ""), "Series,Season,Episode,BoxSet,Movie,MusicVideo,MusicArtist,MusicAlbum,Audio", False, {'MinDateLastSaved': last_sync}): + with database.Database('emby') as embydb: + emby_db = database.emby_db.EmbyDatabase(embydb.cursor) + + for item in data['Items']: + if item['Type'] in self.updated_output: + item['Library'] = {} + item['Library']['Id'] = library + item['Library']['Name'] = emby_db.get_view_name(library) + self.updated_output[item['Type']].put(item) + self.total_updates += 1 + + for data in self.Downloader.get_items(library.replace('Mixed:', ""), "Episode,Movie,MusicVideo,Audio", False, {'MinDateLastSavedForUser': last_sync}): + with database.Database('emby') as embydb: + emby_db = database.emby_db.EmbyDatabase(embydb.cursor) + + for item in data['Items']: + if item['Type'] in self.userdata_output: + item['Library'] = {} + item['Library']['Id'] = library + item['Library']['Name'] = emby_db.get_view_name(library) + self.userdata_output[item['Type']].put(item) + self.total_updates += 1 + + if plugin: + try: + result = self.server['api'].get_sync_queue(last_sync) #Kodi companion plugin + self.removed(result['ItemsRemoved']) + except Exception as error: + self.LOG.exception(error) + + return True + + def save_last_sync(self): + time_now = datetime.utcnow() - timedelta(minutes=2) + last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz') + self.Utils.settings('LastIncrementalSync', value=last_sync) + self.LOG.info("--[ sync/%s ]", last_sync) + + #Select from libraries synced. Either update or repair libraries. + #Send event back to service.py + def select_libraries(self, mode=None): + modes = { + 'SyncLibrarySelection': 'SyncLibrary', + 'RepairLibrarySelection': 'RepairLibrary', + 'AddLibrarySelection': 'SyncLibrary', + 'RemoveLibrarySelection': 'RemoveLibrary' + } + + syncOBJ = database.get_sync() + whitelist = [x.replace('Mixed:', "") for x in syncOBJ['Whitelist']] + libraries = [] + + with database.Database('emby') as embydb: + db = database.emby_db.EmbyDatabase(embydb.cursor) + + if mode in ('SyncLibrarySelection', 'RepairLibrarySelection', 'RemoveLibrarySelection'): + for library in syncOBJ['Whitelist']: + name = db.get_view_name(library.replace('Mixed:', "")) + libraries.append({'Id': library, 'Name': name}) + else: + available = [x for x in syncOBJ['SortedViews'] if x not in whitelist] + + for library in available: + name, media = db.get_view(library) + + if media in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'): + libraries.append({'Id': library, 'Name': name}) + + choices = [x['Name'] for x in libraries] + choices.insert(0, helper.translate._(33121)) + selection = self.Utils.dialog("multi", helper.translate._(33120), choices) + + if selection is None: + return + + if 0 in selection: + selection = list(range(1, len(libraries) + 1)) + + selected_libraries = [] + + for x in selection: + library = libraries[x - 1] + selected_libraries.append(library['Id']) + + self.Utils.event(modes[mode], {'Id': ','.join([libraries[x - 1]['Id'] for x in selection]), 'Update': mode == 'SyncLibrarySelection'}) + + def patch_music(self, notification=False): + try: + with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: + syncObj.patch_music(notification) + except Exception as error: + self.LOG.exception(error) + + def add_library(self, library_id, update=False): + self.add_lib_queue.put((library_id, update)) + self.LOG.info("---[ added library: %s ]", library_id) + + def remove_library(self, library_id): + self.remove_lib_queue.put(library_id) + self.LOG.info("---[ removed library: %s ]", library_id) + + #Add item_id to userdata queue + def userdata(self, data): + if not data: + return + + items = [x['ItemId'] for x in data] + + for item in self.Utils.split_list(items, self.LIMIT): + self.userdata_queue.put(item) + + self.total_updates += len(items) + self.LOG.info("---[ userdata:%s ]", len(items)) + + #Add item_id to updated queue + def updated(self, data): + if not data: + return + + for item in self.Utils.split_list(data, self.LIMIT): + self.updated_queue.put(item) + + self.total_updates += len(data) + self.LOG.info("---[ updated:%s ]", len(data)) + + #Add item_id to removed queue + def removed(self, data): + if not data: + return + + for item in data: + + if item in list(self.removed_queue.queue): + continue + + self.removed_queue.put(item) + + self.total_updates += len(data) + self.LOG.info("---[ removed:%s ]", len(data)) + + #Setup a 1 minute delay for items to be verified + def delay_verify(self, data): + if not data: + return + + time_set = datetime.today() + timedelta(seconds=60) + + for item in data: + self.verify_queue.put((time_set, item,)) + + self.LOG.info("---[ verify:%s ]", len(data)) + +class UpdatedWorker(threading.Thread): + def __init__(self, queue, library, DB, lock): + self.LOG = logging.getLogger("EMBY.library.UpdatedWorker") + self.library = library + self.queue = queue + self.notify = self.library.Utils.settings('newContent.bool') + self.lock = lock + self.DB = database.Database(DB) + self.library.set_progress_dialog() + self.is_done = False + threading.Thread.__init__(self) + self.start() + + def Done(self): + return self.is_done + + def run(self): + with self.lock: + with self.DB as kodidb: + with database.Database('emby') as embydb: + while True: + try: + item = self.queue.get(timeout=1) + except Queue.Empty: + break + + self.library.update_progress_dialog(item) + + try: + if 'Library' in item: + if self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils)[item['Type']](item, item['Library']) and self.notify: + self.library.notify_output.put(item['Type'], self.library.get_naming(item)) + else: + if self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils)[item['Type']](item, None) and self.notify: + self.library.notify_output.put(item['Type'], self.library.get_naming(item)) + except helper.exceptions.LibraryException as error: + if error.status in ('StopCalled', 'StopWriteCalled'): + self.queue.put(item) + break + except Exception as error: + self.LOG.exception(error) + + self.queue.task_done() + + if self.library.Utils.window('emby_should_stop.bool'): + break + + self.LOG.info("--<[ q:updated/%s ]", id(self)) + self.is_done = True + +#Incomming Update Data from Websocket +class UserDataWorker(threading.Thread): + def __init__(self, queue, library, DB, lock): + self.LOG = logging.getLogger("EMBY.library.UserDataWorker") + self.library = library + self.queue = queue + self.lock = lock + self.DB = database.Database(DB) + self.is_done = False + self.library.set_progress_dialog() + threading.Thread.__init__(self) + self.start() + + def Done(self): + return self.is_done + + def run(self): + with self.lock: + with self.DB as kodidb: + with database.Database('emby') as embydb: + while True: + try: + item = self.queue.get(timeout=1) + except Queue.Empty: + break + + self.library.update_progress_dialog(item) + + try: + self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils).userdata(item) + except helper.exceptions.LibraryException as error: + if error.status in ('StopCalled', 'StopWriteCalled'): + self.queue.put(item) + break + except Exception as error: + self.LOG.exception(error) + + self.queue.task_done() + + if self.library.Utils.window('emby_should_stop.bool'): + break + + self.LOG.info("--<[ q:userdata/%s ]", id(self)) + self.is_done = True + +class SortWorker(threading.Thread): + def __init__(self, library): + self.LOG = logging.getLogger("EMBY.library.SortWorker") + self.library = library + self.is_done = False + threading.Thread.__init__(self) + self.start() + + def Done(self): + return self.is_done + + def run(self): + with database.Database('emby') as embydb: + db = database.emby_db.EmbyDatabase(embydb.cursor) + + while True: + try: + item_id = self.library.removed_queue.get(timeout=1) + except Queue.Empty: + break + + try: + media = db.get_media_by_id(item_id) + self.library.removed_output[media].put({'Id': item_id, 'Type': media}) + except Exception: + items = db.get_media_by_parent_id(item_id) + + if not items: + self.LOG.info("Could not find media %s in the emby database.", item_id) + else: + for item in items: + self.library.removed_output[item[1]].put({'Id': item[0], 'Type': item[1]}) + + self.library.removed_queue.task_done() + + if self.library.Utils.window('emby_should_stop.bool'): + break + + self.LOG.info("--<[ q:sort/%s ]", id(self)) + self.is_done = True + +class RemovedWorker(threading.Thread): + def __init__(self, queue, library, DB, lock): + self.LOG = logging.getLogger("EMBY.library.RemovedWorker") + self.library = library + self.queue = queue + self.lock = lock + self.DB = database.Database(DB) + self.is_done = False + threading.Thread.__init__(self) + self.start() + + def Done(self): + return self.is_done + + def run(self): + with self.lock: + with self.DB as kodidb: + with database.Database('emby') as embydb: + while True: + try: + item = self.queue.get(timeout=1) + except Queue.Empty: + break + + try: + self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils).remove(item['Id']) + except helper.exceptions.LibraryException as error: + + if error.status in ('StopCalled', 'StopWriteCalled'): + self.queue.put(item) + break + except Exception as error: + self.LOG.exception(error) + + self.queue.task_done() + + if self.library.Utils.window('emby_should_stop.bool'): + break + + self.LOG.info("--<[ q:removed/%s ]", id(self)) + self.is_done = True + +class NotifyWorker(threading.Thread): + def __init__(self, library): + self.LOG = logging.getLogger("EMBY.library.NotifyWorker") + self.library = library + self.video_time = int(self.library.Utils.settings('newvideotime')) * 1000 + self.music_time = int(self.library.Utils.settings('newmusictime')) * 1000 + self.is_done = False + threading.Thread.__init__(self) + + def Done(self): + return self.is_done + + def run(self): + while True: + try: + item = self.library.notify_output.get(timeout=3) + except Queue.Empty: + break + + time = self.music_time if item[0] == 'Audio' else self.video_time + + if time and (not xbmc.Player().isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')): + self.library.Utils.dialog("notification", heading="%s %s" % (helper.translate._(33049), item[0]), message=item[1], icon="{emby}", time=time, sound=False) + + self.library.notify_output.task_done() + + if self.library.Utils.window('emby_should_stop.bool'): + break + + self.LOG.info("--<[ q:notify/%s ]", id(self)) + self.is_done = True diff --git a/database/queries.py b/database/queries.py new file mode 100644 index 000000000..074761ecb --- /dev/null +++ b/database/queries.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +get_item = """SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type, media_folder, emby_parent_id FROM emby WHERE emby_id = ?""" +get_item_obj = ["{Id}"] +get_item_series_obj = ["{SeriesId}"] +get_item_song_obj = ["{SongAlbumId}"] +get_item_id_by_parent = """SELECT emby_id, kodi_id FROM emby WHERE parent_id = ? AND media_type = ?""" +get_item_id_by_parent_boxset_obj = ["{SetId}", "movie"] +get_item_by_parent = """SELECT emby_id, kodi_id, kodi_fileid FROM emby WHERE parent_id = ? AND media_type = ?""" +get_item_by_media_folder = """SELECT emby_id, emby_type FROM emby WHERE media_folder = ?""" +get_item_by_parent_movie_obj = ["{KodiId}", "movie"] +get_item_by_parent_tvshow_obj = ["{ParentId}", "tvshow"] +get_item_by_parent_season_obj = ["{ParentId}", "season"] +get_item_by_parent_episode_obj = ["{ParentId}", "episode"] +get_item_by_parent_album_obj = ["{ParentId}", "album"] +get_item_by_parent_song_obj = ["{ParentId}", "song"] +get_item_by_wild = """SELECT kodi_id, media_type FROM emby WHERE emby_id LIKE ?""" +get_item_by_wild_obj = ["{Id}"] +get_item_by_kodi = """SELECT emby_id, parent_id, media_folder, emby_type, checksum FROM emby WHERE kodi_id = ? AND media_type = ?""" +get_item_by_kodi_complete = """SELECT * FROM emby WHERE kodi_id = ? AND media_type = ?""" +get_checksum = """SELECT emby_id, checksum FROM emby WHERE emby_type = ?""" +get_view_name = """SELECT view_name FROM view WHERE view_id = ?""" +get_media_by_id = """SELECT emby_type FROM emby WHERE emby_id = ?""" +get_media_by_parent_id = """SELECT emby_id, emby_type, kodi_id, kodi_fileid FROM emby WHERE emby_parent_id = ?""" +get_view = """SELECT view_name, media_type FROM view WHERE view_id = ?""" +get_views = """SELECT * FROM view""" +get_views_by_media = """SELECT * FROM view WHERE media_type = ?""" +get_items_by_media = """SELECT emby_id, checksum FROM emby WHERE media_type = ?""" +get_version = """SELECT idVersion FROM version""" +get_presentation_key = """SELECT emby_id FROM emby WHERE presentation_key LIKE ?""" +add_reference = """INSERT OR REPLACE INTO emby(emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id, checksum, media_folder, emby_parent_id, presentation_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_mediasource_obj = ["{emby_id}", "{MediaIndex}", "{Protocol}", "{Id}", "{Path}", "{Type}", "{Container}", "{Size}", "{Name}", "{IsRemote}", "{RunTimeTicks}", "{SupportsTranscoding}", "{SupportsDirectStream}", "{SupportsDirectPlay}", "{IsInfiniteStream}", "{RequiresOpening}", "{RequiresClosing}", "{RequiresLooping}", "{SupportsProbing}", "{Formats}", "{Bitrate}", "{RequiredHttpHeaders}", "{ReadAtNativeFramerate}", "{DefaultAudioStreamIndex}"] +add_mediasource = """INSERT OR REPLACE INTO MediaSources(emby_id, MediaIndex, Protocol, MediaSourceId, Path, Type, Container, Size, Name, IsRemote, RunTimeTicks, SupportsTranscoding, SupportsDirectStream, SupportsDirectPlay, IsInfiniteStream, RequiresOpening, RequiresClosing, RequiresLooping, SupportsProbing, Formats, Bitrate, RequiredHttpHeaders, ReadAtNativeFramerate, DefaultAudioStreamIndex) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_videostreams_obj = ["{emby_id}", "{MediaIndex}", "{VideoIndex}", "{StreamIndex}", "{Codec}", "{TimeBase}", "{CodecTimeBase}", "{VideoRange}", "{DisplayTitle}", "{IsInterlaced}", "{BitRate}", "{BitDepth}", "{RefFrames}", "{IsDefault}", "{IsForced}", "{Height}", "{Width}", "{AverageFrameRate}", "{RealFrameRate}", "{Profile}", "{Type}", "{AspectRatio}", "{IsExternal}", "{IsTextSubtitleStream}", "{SupportsExternalStream}", "{Protocol}", "{PixelFormat}", "{Level}", "{IsAnamorphic}"] +add_videostreams = """INSERT OR REPLACE INTO VideoStreams(emby_id, MediaIndex, VideoIndex, StreamIndex, Codec, TimeBase, CodecTimeBase, VideoRange, DisplayTitle, IsInterlaced, BitRate, BitDepth, RefFrames, IsDefault, IsForced, Height, Width, AverageFrameRate, RealFrameRate, Profile, Type, AspectRatio, IsExternal, IsTextSubtitleStream, SupportsExternalStream, Protocol, PixelFormat, Level, IsAnamorphic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +get_videostreams = """SELECT * FROM VideoStreams WHERE emby_id = ? AND MediaIndex = ?""" +get_mediasourceid = """SELECT Id FROM MediaSources WHERE emby_id = ?""" +get_mediasource = """SELECT * FROM MediaSources WHERE emby_id = ?""" +get_kodiid = """SELECT kodi_id, presentation_key FROM emby WHERE emby_id = ?""" +get_AudioStreams = """SELECT * FROM AudioStreams WHERE emby_id = ? AND MediaIndex = ?""" +get_Subtitles = """SELECT * FROM Subtitle WHERE emby_id = ? AND MediaIndex = ?""" +get_kodifileid = """SELECT kodi_fileid FROM emby WHERE emby_id = ?""" +get_embyid_by_kodiid = """SELECT emby_id FROM emby WHERE kodi_id = ? AND media_type = ?""" +add_audiostreams_obj = ["{emby_id}", "{MediaIndex}", "{AudioIndex}", "{StreamIndex}", "{Codec}", "{Language}", "{TimeBase}", "{CodecTimeBase}", "{DisplayTitle}", "{DisplayLanguage}", "{IsInterlaced}", "{ChannelLayout}", "{BitRate}", "{Channels}", "{SampleRate}", "{IsDefault}", "{IsForced}", "{Profile}", "{Type}", "{IsExternal}", "{IsTextSubtitleStream}", "{SupportsExternalStream}", "{Protocol}"] +add_audiostreams = """INSERT OR REPLACE INTO AudioStreams(emby_id, MediaIndex, AudioIndex, StreamIndex, Codec, Language, TimeBase, CodecTimeBase, DisplayTitle, DisplayLanguage, IsInterlaced, ChannelLayout, BitRate, Channels, SampleRate, IsDefault, IsForced, Profile, Type, IsExternal, IsTextSubtitleStream, SupportsExternalStream, Protocol) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_subtitles_obj = ["{emby_id}", "{MediaIndex}", "{SubtitleIndex}", "{StreamIndex}", "{Codec}", "{Language}", "{TimeBase}", "{CodecTimeBase}", "{DisplayTitle}", "{DisplayLanguage}", "{IsInterlaced}", "{IsDefault}", "{IsForced}", "{Path}", "{Type}", "{IsExternal}", "{IsTextSubtitleStream}", "{SupportsExternalStream}", "{Protocol}"] +add_subtitles = """INSERT OR REPLACE INTO Subtitle(emby_id, MediaIndex, SubtitleIndex, StreamIndex, Codec, Language, TimeBase, CodecTimeBase, DisplayTitle, DisplayLanguage, IsInterlaced, IsDefault, IsForced, Path, Type, IsExternal, IsTextSubtitleStream, SupportsExternalStream, Protocol) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_reference_movie_obj = ["{Id}", "{MovieId}", "{FileId}", "{PathId}", "Movie", "movie", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] +add_reference_boxset_obj = ["{Id}", "{SetId}", None, None, "BoxSet", "set", None, "{Checksum}", None, None, "{PresentationKey}"] +add_reference_tvshow_obj = ["{Id}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] +add_reference_season_obj = ["{Id}", "{SeasonId}", None, None, "Season", "season", "{ShowId}", None, "{LibraryId}", None, "{PresentationKey}"] +add_reference_pool_obj = ["{SeriesId}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", None, "{PresentationKey}"] +add_reference_episode_obj = ["{Id}", "{EpisodeId}", "{FileId}", "{PathId}", "Episode", "episode", "{SeasonId}", "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] +add_reference_mvideo_obj = ["{Id}", "{MvideoId}", "{FileId}", "{PathId}", "MusicVideo", "musicvideo", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] +add_reference_artist_obj = ["{Id}", "{ArtistId}", None, None, "{ArtistType}", "artist", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] +add_reference_album_obj = ["{Id}", "{AlbumId}", None, None, "MusicAlbum", "album", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] +add_reference_song_obj = ["{Id}", "{SongId}", None, "{PathId}", "Audio", "song", "{AlbumId}", "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] +add_view = """INSERT OR REPLACE INTO view(view_id, view_name, media_type) VALUES (?, ?, ?)""" +add_version = """INSERT OR REPLACE INTO version(idVersion) VALUES (?)""" +update_reference = """ UPDATE emby SET checksum = ?, presentation_key = ? WHERE emby_id = ?""" +update_reference_obj = ["{Checksum}", "{PresentationKey}", "{Id}"] +update_parent = """ UPDATE emby SET parent_id = ? WHERE emby_id = ?""" +update_parent_movie_obj = ["{SetId}", "{Id}"] +update_parent_episode_obj = ["{SeasonId}", "{Id}"] +update_parent_album_obj = ["{ArtistId}", "{AlbumId}"] +delete_item = """DELETE FROM emby WHERE emby_id = ?""" +delete_mediasources = """DELETE FROM MediaSources WHERE emby_id = ?""" +delete_videostreams = """DELETE FROM VideoStreams WHERE emby_id = ?""" +delete_audiostreams = """DELETE FROM AudioStreams WHERE emby_id = ?""" +delete_subtitles = """DELETE FROM Subtitle WHERE emby_id = ?""" +delete_item_obj = ["{Id}"] +delete_item_by_parent = """DELETE FROM emby WHERE parent_id = ? AND media_type = ?""" +delete_item_by_parent_tvshow_obj = ["{ParentId}", "tvshow"] +delete_item_by_parent_season_obj = ["{ParentId}", "season"] +delete_item_by_parent_episode_obj = ["{ParentId}", "episode"] +delete_item_by_parent_song_obj = ["{ParentId}", "song"] +delete_item_by_parent_artist_obj = ["{ParentId}", "artist"] +delete_item_by_parent_album_obj = ["{KodiId}", "album"] +delete_item_by_kodi = """DELETE FROM emby WHERE kodi_id = ? AND media_type = ?""" +delete_item_by_wild = """DELETE FROM emby WHERE emby_id LIKE ?""" +delete_view = """DELETE FROM view WHERE view_id = ?""" +delete_parent_boxset_obj = [None, "{Movie}"] +delete_media_by_parent_id = """DELETE FROM emby WHERE emby_parent_id = ?""" +delete_version = """DELETE FROM version""" diff --git a/database/sync.py b/database/sync.py new file mode 100644 index 000000000..27056d3c3 --- /dev/null +++ b/database/sync.py @@ -0,0 +1,437 @@ +# -*- coding: utf-8 -*- +import datetime +import logging +import xbmc + +import helper.translate +import helper.wrapper +import helper.exceptions +import helper.xmls +from . import database +from . import emby_db + +class Sync(): + running = False + + def __init__(self, library, server, Downloader, Utils): + self.LOG = logging.getLogger("EMBY.sync") + self.sync = None +# self.running = False + self.screensaver = None + self.update_library = False + self.Downloader = Downloader + self.Utils = Utils + self.xmls = helper.xmls.Xmls(self.Utils) + self.direct_path = self.Utils.settings('useDirectPaths') == "1" + + if self.running: + self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33197)) + raise Exception("Sync is already running.") + + self.library = library + self.server = server + + #Do everything we need before the sync + def __enter__(self): + self.LOG.info("-->[ fullsync ]") + + if not self.Utils.settings('dbSyncScreensaver.bool'): + xbmc.executebuiltin('InhibitIdleShutdown(true)') + self.screensaver = self.Utils.get_screensaver() + self.Utils.set_screensaver(value="") + + self.running = True + self.Utils.window('emby_sync.bool', True) + return self + + #Assign the restore point and save the sync status + def _restore_point(self, restore): + self.sync['RestorePoint'] = restore + database.save_sync(self.sync) + + #Map the syncing process and start the sync. Ensure only one sync is running + #force to resume any previous sync + def libraries(self, library_id=None, update=False, forced=False): + self.update_library = update + self.sync = database.get_sync() + + if library_id: + libraries = library_id.split(',') + + for selected in libraries: + if selected not in [x.replace('Mixed:', "") for x in self.sync['Libraries']]: + library = self.get_libraries(selected) + + if library: + self.sync['Libraries'].append("Mixed:%s" % selected if library[1] == 'mixed' else selected) + + if library[1] in ('mixed', 'movies'): + self.sync['Libraries'].append('Boxsets:%s' % selected) + else: + self.sync['Libraries'].append(selected) + else: + self.mapping(forced) + + self.xmls.sources() + + if not self.xmls.advanced_settings() and self.sync['Libraries']: + self.start() + + def get_libraries(self, library_id=None): + with database.Database('emby') as embydb: + if library_id is None: + return emby_db.EmbyDatabase(embydb.cursor).get_views() + + return emby_db.EmbyDatabase(embydb.cursor).get_view(library_id) + + #Load the mapping of the full sync. + #This allows us to restore a previous sync + def mapping(self, forced=False): + if self.sync['Libraries']: + if not forced and not self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33102)): + if not self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33173)): + self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33122)) + self.Utils.window('emby_sync_skip_resume.bool', True) + raise helper.exceptions.LibraryException("StopWriteCalled") + + self.sync['Libraries'] = [] + self.sync['RestorePoint'] = {} + else: + self.LOG.info("generate full sync") + libraries = [] + + for library in self.get_libraries(): + if library[2] in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed'): + libraries.append({'Id': library[0], 'Name': library[1], 'Media': library[2]}) + + libraries = self.select_libraries(libraries) + + if [x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed')]: + self.sync['Libraries'].append("Boxsets:") + + database.save_sync(self.sync) + + #Select all or certain libraries to be whitelisted + def select_libraries(self, libraries): + if self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33125), nolabel=helper.translate._(33127), yeslabel=helper.translate._(33126)): + self.LOG.info("Selected sync later.") + raise helper.exceptions.LibraryException('SyncLibraryLater') + + choices = [x['Name'] for x in libraries] + choices.insert(0, helper.translate._(33121)) + selection = self.Utils.dialog("multi", helper.translate._(33120), choices) + + if selection is None: + raise helper.exceptions.LibraryException('LibrarySelection') + + if not selection: + self.LOG.info("Nothing was selected.") + raise helper.exceptions.LibraryException('SyncLibraryLater') + + if 0 in selection: + selection = list(range(1, len(libraries) + 1)) + + selected_libraries = [] + + for x in selection: + library = libraries[x - 1] + + if library['Media'] != 'mixed': + selected_libraries.append(library['Id']) + else: + selected_libraries.append("Mixed:%s" % library['Id']) + + self.sync['Libraries'] = selected_libraries + return [libraries[x - 1] for x in selection] + + #Main sync process + def start(self): + self.LOG.info("starting sync with %s", self.sync['Libraries']) + database.save_sync(self.sync) + start_time = datetime.datetime.now() + + for library in list(self.sync['Libraries']): + self.process_library(library) + + if not library.startswith('Boxsets:') and library not in self.sync['Whitelist']: + self.sync['Whitelist'].append(library) + + self.sync['Libraries'].pop(self.sync['Libraries'].index(library)) + self._restore_point({}) + + elapsed = datetime.datetime.now() - start_time + self.Utils.settings('SyncInstallRunDone.bool', True) + self.library.save_last_sync() + database.save_sync(self.sync) + xbmc.executebuiltin('UpdateLibrary(video)') + self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (helper.translate._(33025), str(elapsed).split('.')[0]), icon="{emby}", sound=False) + self.LOG.info("Full sync completed in: %s", str(elapsed).split('.')[0]) + + #Add a library by it's id. Create a node and a playlist whenever appropriate + def process_library(self, library_id): + media = { + 'movies': self.movies, + 'musicvideos': self.musicvideos, + 'tvshows': self.tvshows, + 'music': self.music + } + + try: + if library_id.startswith('Boxsets:'): + if library_id.endswith('Refresh'): + self.refresh_boxsets() + else: + self.boxsets(library_id.split('Boxsets:')[1] if len(library_id) > len('Boxsets:') else None) + + return + + library = self.server['api'].get_item(library_id.replace('Mixed:', "")) + + if library_id.startswith('Mixed:'): + for mixed in ('movies', 'tvshows'): + media[mixed](library, self.Downloader) + self.sync['RestorePoint'] = {} + else: + if library['CollectionType']: + self.Utils.settings('enableMusic.bool', True) + + media[library['CollectionType']](library, self.Downloader) + except helper.exceptions.LibraryException as error: + if error.status in ('StopCalled', 'StopWriteCalled'): + database.save_sync(self.sync) + raise + except Exception as error: + if 'Failed to validate path' not in error.args: + self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33119)) + self.LOG.error("full sync exited unexpectedly") + database.save_sync(self.sync) + + raise + + #Process movies from a single library + @helper.wrapper.progress() + def movies(self, library, Downloader, dialog): + with self.library.database_lock: + with database.Database() as videodb: + with database.Database('emby') as embydb: + MoviesObject = self.library.MEDIA['Movie'](self.server, embydb, videodb, self.direct_path, self.Utils) + TotalRecords = Downloader.get_TotalRecordsRegular(library['Id'], "Movie") + + for items in Downloader.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')): + self._restore_point(items['RestorePoint']) + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, movie in enumerate(items['Items']): + dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=movie['Name']) + MoviesObject.movie(movie, library=library) + + #Compare entries from library to what's in the embydb. Remove surplus + if self.update_library: + items = emby_db.EmbyDatabase(embydb.cursor).get_item_by_media_folder(library['Id']) + current = MoviesObject.item_ids + + for x in items: + if x[0] not in current and x[1] == 'Movie': + MoviesObject.remove(x[0]) + + #Process tvshows and episodes from a single library + @helper.wrapper.progress() + def tvshows(self, library, Downloader, dialog): + with self.library.database_lock: + with database.Database() as videodb: + with database.Database('emby') as embydb: + TVShowsObject = self.library.MEDIA['TVShow'](self.server, embydb, videodb, self.direct_path, self.Utils, True) + TotalRecords = Downloader.get_TotalRecordsRegular(library['Id'], "Series") + + for items in Downloader.get_items(library['Id'], "Series", False, self.sync['RestorePoint'].get('params')): + self._restore_point(items['RestorePoint']) + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, show in enumerate(items['Items']): + percent = int((float(start_index + index) / TotalRecords)*100) + dialog.update(percent, heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=show['Name']) + + if TVShowsObject.tvshow(show, library=library): + for episodes in Downloader.get_episode_by_show(show['Id']): + for episode in episodes['Items']: + dialog.update(percent, message="%s/%s" % (show['Name'], episode['Name'][:10])) + TVShowsObject.episode(episode, library=library) + + #Compare entries from library to what's in the embydb. Remove surplus + if self.update_library: + items = emby_db.EmbyDatabase(embydb.cursor).get_item_by_media_folder(library['Id']) + + for x in list(items): + items.extend(TVShowsObject.get_child(x[0])) + + current = TVShowsObject.item_ids + + for x in items: + if x[0] not in current and x[1] == 'Series': + TVShowsObject.remove(x[0]) + + #Process musicvideos from a single library + @helper.wrapper.progress() + def musicvideos(self, library, Downloader, dialog): + with self.library.database_lock: + with database.Database() as videodb: + with database.Database('emby') as embydb: + MusicVideosObject = self.library.MEDIA['MusicVideo'](self.server, embydb, videodb, self.direct_path, self.Utils) + TotalRecords = Downloader.get_TotalRecordsRegular(library['Id'], "MusicVideo") + + for items in Downloader.get_items(library['Id'], "MusicVideo", False, self.sync['RestorePoint'].get('params')): + self._restore_point(items['RestorePoint']) + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, mvideo in enumerate(items['Items']): + dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=mvideo['Name']) + MusicVideosObject.musicvideo(mvideo, library=library) + + #Compare entries from library to what's in the embydb. Remove surplus + if self.update_library: + items = emby_db.EmbyDatabase(embydb.cursor).get_item_by_media_folder(library['Id']) + current = MusicVideosObject.item_ids + + for x in items: + if x[0] not in current and x[1] == 'MusicVideo': + MusicVideosObject.remove(x[0]) + + #Process artists, album, songs from a single library + @helper.wrapper.progress() + def music(self, library, Downloader, dialog): + self.patch_music() + + with self.library.music_database_lock: + with database.Database('music') as musicdb: + with database.Database('emby') as embydb: + MusicObject = self.library.MEDIA['Music'](self.server, embydb, musicdb, self.direct_path, self.Utils) + TotalRecords = Downloader.get_TotalRecordsArtists(library['Id']) + + for items in Downloader.get_artists(library['Id'], False, self.sync['RestorePoint'].get('params')): + self._restore_point(items['RestorePoint']) + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, artist in enumerate(items['Items']): + percent = int((float(start_index + index) / TotalRecords) * 100) + dialog.update(percent, heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=artist['Name']) + MusicObject.artist(artist, library=library) + + for albums in Downloader.get_albums_by_artist(library['Id'], artist['Id']): + for album in albums['Items']: + MusicObject.album(album, library=library) + + for songs in Downloader.get_songs_by_artist(library['Id'], artist['Id']): + for song in songs['Items']: + MusicObject.song(song, library=library) + + #Compare entries from library to what's in the embydb. Remove surplus + if self.update_library: + items = emby_db.EmbyDatabase(embydb.cursor).get_item_by_media_folder(library['Id']) + + for x in list(items): + items.extend(MusicObject.get_child(x[0])) + + current = MusicObject.item_ids + + for x in items: + if x[0] not in current and x[1] == 'MusicArtist': + MusicObject.remove(x[0]) + + #Process all boxsets + @helper.wrapper.progress(helper.translate._(33018)) + def boxsets(self, library_id=None, dialog=None): + with self.library.database_lock: + with database.Database() as videodb: + with database.Database('emby') as embydb: + MoviesObject = self.library.MEDIA['Movie'](self.server, embydb, videodb, self.direct_path, self.Utils) + TotalRecords = self.Downloader.get_TotalRecordsRegular(library_id, "BoxSet") + + for items in self.Downloader.get_items(library_id, "BoxSet", False, self.sync['RestorePoint'].get('params')): + self._restore_point(items['RestorePoint']) + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, boxset in enumerate(items['Items']): + dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (helper.translate._('addon_name'), helper.translate._('boxsets')), message=boxset['Name']) + MoviesObject.boxset(boxset) + + #Delete all exisitng boxsets and re-add + def refresh_boxsets(self): + with self.library.database_lock: + with database.Database() as videodb: + with database.Database('emby') as embydb: + MoviesObject = self.library.MEDIA['Movie'](self.server, embydb, videodb, self.direct_path, self.Utils) + MoviesObject.boxsets_reset() + + self.boxsets(None) + + #Patch the music database to silence the rescan prompt + def patch_music(self, notification=False): + with self.library.database_lock: + with database.Database('music') as musicdb: + self.library.MEDIA['MusicDisableScan'](musicdb.cursor, int(self.Utils.window('kodidbverion.music'))).disable_rescan() + + self.Utils.settings('MusicRescan.bool', True) + + if notification: + self.Utils.dialog("notification", heading="{emby}", message=helper.translate._('task_success'), icon="{emby}", time=1000, sound=False) + + #Remove library by their id from the Kodi database + @helper.wrapper.progress(helper.translate._(33144)) + def remove_library(self, library_id, dialog=None): + direct_path = self.library.direct_path + + with database.Database('emby') as embydb: + db = emby_db.EmbyDatabase(embydb.cursor) + library = db.get_view(library_id.replace('Mixed:', "")) + items = db.get_item_by_media_folder(library_id.replace('Mixed:', "")) + media = 'music' if library[1] == 'music' else 'video' + + if items: + count = 0 + + with self.library.music_database_lock if media == 'music' else self.library.database_lock: + with database.Database(media) as kodidb: + if library[1] == 'mixed': + movies = [x for x in items if x[1] == 'Movie'] + tvshows = [x for x in items if x[1] == 'Series'] + MediaObject = self.library.MEDIA['Movie'](self.server, embydb, kodidb, direct_path, self.Utils).remove + + for item in movies: + MediaObject(item[0]) + dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (helper.translate._('addon_name'), library[0])) + count += 1 + + MediaObject = self.library.MEDIA['Series'](self.server, embydb, kodidb, direct_path, self.Utils).remove + + for item in tvshows: + MediaObject(item[0]) + dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (helper.translate._('addon_name'), library[0])) + count += 1 + else: + MediaObject = self.library.MEDIA[items[0][1]](self.server, embydb, kodidb, direct_path, self.Utils).remove + + for item in items: + MediaObject(item[0]) + dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (helper.translate._('addon_name'), library[0])) + count += 1 + + self.sync = database.get_sync() + + if library_id in self.sync['Whitelist']: + self.sync['Whitelist'].remove(library_id) + elif 'Mixed:%s' % library_id in self.sync['Whitelist']: + self.sync['Whitelist'].remove('Mixed:%s' % library_id) + + database.save_sync(self.sync) + + #Exiting sync + def __exit__(self, exc_type, exc_val, exc_tb): + self.running = False + self.Utils.window('emby_sync', clear=True) + + if self.screensaver is not None: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + self.Utils.set_screensaver(value=self.screensaver) + self.screensaver = None + + self.LOG.info("--<[ fullsync ]") diff --git a/default.py b/default.py deleted file mode 100644 index 4f0e6741e..000000000 --- a/default.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import os -import sys - -import xbmc -import xbmcaddon - -################################################################################################# - -__addon__ = xbmcaddon.Addon(id='plugin.video.emby') -__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') -__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8') -__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') -__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') - -sys.path.insert(0, __cache__) -sys.path.insert(0, __pcache__) -sys.path.insert(0, __libraries__) -sys.path.append(__base__) - -################################################################################################# -from helper import window - -#Verify emby for kodi plugin is fully loaded, timeout after 30 seconds -EmbyOnline = False - -for i in range(60): - if window('emby_online.bool'): - EmbyOnline = True - from entrypoint import Events - break - else: - xbmc.sleep(500) - -if not EmbyOnline: - exit() - -################################################################################################# - -LOG = logging.getLogger("EMBY.default") - -################################################################################################# - - -if __name__ == "__main__": - - LOG.debug("--->[ default ]") - - try: - Events() - except Exception as error: - LOG.exception(error) - - LOG.info("---<[ default ]") diff --git a/libraries/requests/packages/urllib3/contrib/__init__.py b/dialogs/__init__.py similarity index 100% rename from libraries/requests/packages/urllib3/contrib/__init__.py rename to dialogs/__init__.py diff --git a/dialogs/context.py b/dialogs/context.py new file mode 100644 index 000000000..9618b2806 --- /dev/null +++ b/dialogs/context.py @@ -0,0 +1,60 @@ +import logging +import os + +import xbmcgui +import xbmcaddon +import helper.utils + +class ContextMenu(xbmcgui.WindowXMLDialog): + def __init__(self, *args, **kwargs): + self._options = [] + self.selected_option = None + self.list_ = None + self.LOG = logging.getLogger("EMBY.dialogs.context.ContextMenu") + self.Utils = helper.utils.Utils() + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_options(self, options=[]): + self._options = options + + def is_selected(self): + return bool(self.selected_option) + + def get_selected(self): + return self.selected_option + + def onInit(self): + if self.Utils.window('EmbyUserImage'): + self.getControl(150).setImage(self.Utils.window('EmbyUserImage')) + + self.LOG.info("options: %s", self._options) + self.list_ = self.getControl(155) + + for option in self._options: + self.list_.addItem(self._add_listitem(option)) + + self.setFocus(self.list_) + + def onAction(self, action): + if action in (92, 9, 10): + self.close() + + if action in (7, 100): + if self.getFocusId() == 155: + option = self.list_.getSelectedItem() + self.selected_option = option.getLabel() + self.LOG.info('option selected: %s', self.selected_option) + self.close() + + def _add_editcontrol(self, x, y, height, width): + media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlImage(0, 0, 0, 0, filename=os.path.join(media, "white.png"), aspectRatio=0, colorDiffuse="ff111111") + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + self.addControl(control) + return control + + @classmethod + def _add_listitem(cls, label): + return xbmcgui.ListItem(label) diff --git a/resources/lib/dialogs/loginconnect.py b/dialogs/loginconnect.py similarity index 63% rename from resources/lib/dialogs/loginconnect.py rename to dialogs/loginconnect.py index ed4904228..59d44eb39 100644 --- a/resources/lib/dialogs/loginconnect.py +++ b/dialogs/loginconnect.py @@ -1,18 +1,9 @@ # -*- coding: utf-8 -*- - -################################################################################################## - import logging -import os - import xbmcgui -import xbmcaddon - -from helper import _, addon_id, settings, dialog +import helper.utils +import helper.translate -################################################################################################## - -LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -25,33 +16,32 @@ 'Empty': 2 } -################################################################################################## - - class LoginConnect(xbmcgui.WindowXMLDialog): - - _user = None - error = None - - def __init__(self, *args, **kwargs): - + self._user = None + self.error = None + self.LOG = logging.getLogger("EMBY.dialogs.loginconnect.LoginConnect") + self.Utils = helper.utils.Utils() + self.user_field = None + self.password_field = None + self.signin_button = None + self.remind_button = None + self.error_toggle = None + self.error_msg = None xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) def set_args(self, **kwargs): # connect_manager, user_image, servers, emby_connect - for key, value in kwargs.iteritems(): + for key, value in list(kwargs.items()): setattr(self, key, value) def is_logged_in(self): - return True if self._user else False + return bool(self._user) def get_user(self): return self._user - def onInit(self): - self.user_field = self._add_editcontrol(755, 338, 40, 415) self.setFocus(self.user_field) self.password_field = self._add_editcontrol(755, 448, 40, 415, password=1) @@ -59,7 +49,6 @@ def onInit(self): self.remind_button = self.getControl(CANCEL) self.error_toggle = self.getControl(ERROR_TOGGLE) self.error_msg = self.getControl(ERROR_MSG) - self.user_field.controlUp(self.remind_button) self.user_field.controlDown(self.password_field) self.password_field.controlUp(self.user_field) @@ -68,28 +57,23 @@ def onInit(self): self.remind_button.controlDown(self.user_field) def onClick(self, control): - if control == SIGN_IN: # Sign in to emby connect self._disable_error() - user = self.user_field.getText() password = self.password_field.getText() if not user or not password: # Display error - self._error(ERROR['Empty'], _('empty_user_pass')) - LOG.error("Username or password cannot be null") - + self._error(ERROR['Empty'], helper.translate._('empty_user_pass')) + self.LOG.error("Username or password cannot be null") elif self._login(user, password): self.close() - elif control == CANCEL: # Remind me later self.close() def onAction(self, action): - if (self.error == ERROR['Empty'] and self.user_field.getText() and self.password_field.getText()): self._disable_error() @@ -98,50 +82,34 @@ def onAction(self, action): self.close() def _add_editcontrol(self, x, y, height, width, password=0): - - media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') - control = xbmcgui.ControlEdit(0, 0, 0, 0, - label="User", - font="font13", - textColor="FF52b54b", - disabledColor="FF888888", - focusTexture="-", - noFocusTexture="-", - isPassword=password) +# media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + #####control = xbmcgui.ControlEdit(0, 0, 0, 0, label="User", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-", isPassword=password) + control = xbmcgui.ControlEdit(0, 0, 0, 0, label="", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-") control.setPosition(x, y) control.setHeight(height) control.setWidth(width) - self.addControl(control) return control def _login(self, username, password): - result = self.connect_manager['login-connect'](username, password) - if result is False: - self._error(ERROR['Invalid'], _('invalid_auth')) + if result is False: + self._error(ERROR['Invalid'], helper.translate._('invalid_auth')) return False self._user = result username = result['User']['Name'] - settings('connectUsername', value=username) - settings('idMethod', value="1") - - dialog("notification", heading="{emby}", message="%s %s" % (_(33000), username.decode('utf-8')), - icon=result['User'].get('ImageUrl') or "{emby}", - time=2000, - sound=False) - + self.Utils.settings('connectUsername', value=username) + self.Utils.settings('idMethod', value="1") + self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (helper.translate._(33000), self.Utils.StringMod(username)), icon=result['User'].get('ImageUrl') or "{emby}", time=2000, sound=False) return True def _error(self, state, message): - self.error = state self.error_msg.setLabel(message) self.error_toggle.setVisibleCondition('true') def _disable_error(self): - self.error = None self.error_toggle.setVisibleCondition('false') diff --git a/resources/lib/dialogs/loginmanual.py b/dialogs/loginmanual.py similarity index 67% rename from resources/lib/dialogs/loginmanual.py rename to dialogs/loginmanual.py index c9ad82fe1..107daeab3 100644 --- a/resources/lib/dialogs/loginmanual.py +++ b/dialogs/loginmanual.py @@ -1,18 +1,9 @@ # -*- coding: utf-8 -*- - -################################################################################################## - import logging -import os - import xbmcgui -import xbmcaddon - -from helper import _, addon_id +import helper.utils +import helper.translate -################################################################################################## - -LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -22,32 +13,33 @@ ERROR_MSG = 203 ERROR = {'Invalid': 1, 'Empty': 2} -################################################################################################## - - class LoginManual(xbmcgui.WindowXMLDialog): - - _user = None - error = None - username = None - - def __init__(self, *args, **kwargs): + self._user = None + self.error = None + self.username = None + self.LOG = logging.getLogger("EMBY.dialogs.loginmanual.LoginManual") + self.Utils = helper.utils.Utils() + self.user_field = None + self.password_field = None + self.signin_button = None + self.error_toggle = None + self.error_msg = None + self.cancel_button = None xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) def set_args(self, **kwargs): # connect_manager, user_image, servers, emby_connect - for key, value in kwargs.iteritems(): + for key, value in list(kwargs.items()): setattr(self, key, value) def is_logged_in(self): - return True if self._user else False + return bool(self._user) def get_user(self): return self._user def onInit(self): - self.signin_button = self.getControl(SIGN_IN) self.cancel_button = self.getControl(CANCEL) self.error_toggle = self.getControl(ERROR_TOGGLE) @@ -56,7 +48,6 @@ def onInit(self): self.password_field = self._add_editcontrol(755, 543, 40, 415, password=1) if self.username: - self.user_field.setText(self.username) self.setFocus(self.password_field) else: @@ -70,28 +61,23 @@ def onInit(self): self.cancel_button.controlDown(self.user_field) def onClick(self, control): - if control == SIGN_IN: # Sign in to emby connect self._disable_error() - user = self.user_field.getText() password = self.password_field.getText() if not user: # Display error - self._error(ERROR['Empty'], _('empty_user')) - LOG.error("Username cannot be null") - + self._error(ERROR['Empty'], helper.translate._('empty_user')) + self.LOG.error("Username cannot be null") elif self._login(user, password): self.close() - elif control == CANCEL: # Remind me later self.close() def onAction(self, action): - if self.error == ERROR['Empty'] and self.user_field.getText(): self._disable_error() @@ -99,44 +85,32 @@ def onAction(self, action): self.close() def _add_editcontrol(self, x, y, height, width, password=0): - - media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') - control = xbmcgui.ControlEdit(0, 0, 0, 0, - label="User", - font="font13", - textColor="FF52b54b", - disabledColor="FF888888", - focusTexture="-", - noFocusTexture="-", - isPassword=password) +# media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') +#### control = xbmcgui.ControlEdit(0, 0, 0, 0, label="User", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-", isPassword=password) + control = xbmcgui.ControlEdit(0, 0, 0, 0, label="", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-") +# control.setInputType(xbmcgui.INPUT_TYPE_PASSWORD) control.setPosition(x, y) control.setHeight(height) control.setWidth(width) - self.addControl(control) - return control def _login(self, username, password): - - mode = self.connect_manager['server-mode'] server = self.connect_manager['server-address'] result = self.connect_manager['login'](server, username, password) if not result: - self._error(ERROR['Invalid'], _('invalid_auth')) + self._error(ERROR['Invalid'], helper.translate._('invalid_auth')) return False - else: - self._user = result - return True - def _error(self, state, message): + self._user = result + return True + def _error(self, state, message): self.error = state self.error_msg.setLabel(message) self.error_toggle.setVisibleCondition('true') def _disable_error(self): - self.error = None self.error_toggle.setVisibleCondition('false') diff --git a/resources/lib/dialogs/serverconnect.py b/dialogs/serverconnect.py similarity index 72% rename from resources/lib/dialogs/serverconnect.py rename to dialogs/serverconnect.py index 2b79c497a..4ccad7bf9 100644 --- a/resources/lib/dialogs/serverconnect.py +++ b/dialogs/serverconnect.py @@ -1,18 +1,10 @@ # -*- coding: utf-8 -*- - -################################################################################################## - import logging - import xbmc import xbmcgui +import emby.core.connection_manager +import helper.translate -from helper import _ -from emby.core.connection_manager import CONNECTION_STATE - -################################################################################################## - -LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -27,30 +19,27 @@ EMBY_CONNECT = 205 MANUAL_SERVER = 206 -################################################################################################## - - class ServerConnect(xbmcgui.WindowXMLDialog): - - user_image = None - servers = [] - - _selected_server = None - _connect_login = False - _manual_server = False - - def __init__(self, *args, **kwargs): - + self.user_image = None + self.servers = [] + self._selected_server = None + self._connect_login = False + self._manual_server = False + self.message = None + self.message_box = None + self.busy = None + self.list_ = None + self.LOG = logging.getLogger("EMBY.dialogs.serverconnect.ServerConnect") xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + #connect_manager, user_image, servers, emby_connect def set_args(self, **kwargs): - # connect_manager, user_image, servers, emby_connect - for key, value in kwargs.iteritems(): + for key, value in list(kwargs.items()): setattr(self, key, value) def is_server_selected(self): - return True if self._selected_server else False + return bool(self._selected_server) def get_server(self): return self._selected_server @@ -61,9 +50,7 @@ def is_connect_login(self): def is_manual_server(self): return self._manual_server - def onInit(self): - self.message = self.getControl(MESSAGE) self.message_box = self.getControl(MESSAGE_BOX) self.busy = self.getControl(BUSY) @@ -77,66 +64,55 @@ def onInit(self): self.getControl(USER_IMAGE).setImage(self.user_image) if not self.emby_connect: # Change connect user - self.getControl(EMBY_CONNECT).setLabel("[B]%s[/B]" % _(30618)) + self.getControl(EMBY_CONNECT).setLabel("[B]%s[/B]" % helper.translate._(30618)) if self.servers: self.setFocus(self.list_) @classmethod def _add_listitem(cls, label, server_id, server_type): - item = xbmcgui.ListItem(label) item.setProperty('id', server_id) item.setProperty('server_type', server_type) - return item def onAction(self, action): - if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): self.close() if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): - if self.getFocusId() == LIST: server = self.list_.getSelectedItem() selected_id = server.getProperty('id') - LOG.info('Server Id selected: %s', selected_id) + self.LOG.info('Server Id selected: %s', selected_id) if self._connect_server(selected_id): self.message_box.setVisibleCondition('false') self.close() def onClick(self, control): - if control == EMBY_CONNECT: self.connect_manager.clear_data() self._connect_login = True self.close() - elif control == MANUAL_SERVER: self._manual_server = True self.close() - elif control == CANCEL: self.close() def _connect_server(self, server_id): - server = self.connect_manager.get_server_info(server_id) - self.message.setLabel("%s %s..." % (_(30610), server['Name'])) - + self.message.setLabel("%s %s..." % (helper.translate._(30610), server['Name'])) self.message_box.setVisibleCondition('true') self.busy.setVisibleCondition('true') - result = self.connect_manager['connect-to-server'](server) - if result['State'] == CONNECTION_STATE['Unavailable']: + if result['State'] == emby.core.connection_manager.CONNECTION_STATE['Unavailable']: self.busy.setVisibleCondition('false') - - self.message.setLabel(_(30609)) + self.message.setLabel(helper.translate._(30609)) return False - else: - xbmc.sleep(1000) - self._selected_server = result['Servers'][0] - return True + + xbmc.sleep(1000) + self._selected_server = result['Servers'][0] + return True diff --git a/resources/lib/dialogs/servermanual.py b/dialogs/servermanual.py similarity index 65% rename from resources/lib/dialogs/servermanual.py rename to dialogs/servermanual.py index 45eebcf31..57a453d5c 100644 --- a/resources/lib/dialogs/servermanual.py +++ b/dialogs/servermanual.py @@ -1,19 +1,10 @@ # -*- coding: utf-8 -*- - -################################################################################################## - import logging -import os - import xbmcgui -import xbmcaddon - -from helper import _, addon_id -from emby.core.connection_manager import CONNECTION_STATE +import emby.core.connection_manager +import helper.utils +import helper.translate -################################################################################################## - -LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -26,42 +17,40 @@ 'Empty': 2 } -################################################################################################## - - class ServerManual(xbmcgui.WindowXMLDialog): - - _server = None - error = None - - def __init__(self, *args, **kwargs): - + self._server = None + self.error = None + self.connect_button = None + self.cancel_button = None + self.error_toggle = None + self.error_msg = None + self.host_field = None + self.port_field = None + self.LOG = logging.getLogger("EMBY.dialogs.servermanual.ServerManual") + self.Utils = helper.utils.Utils() xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + #connect_manager, user_image, servers, emby_connect def set_args(self, **kwargs): - # connect_manager, user_image, servers, emby_connect - for key, value in kwargs.iteritems(): + for key, value in list(kwargs.items()): setattr(self, key, value) def is_connected(self): - return True if self._server else False + return bool(self._server) def get_server(self): return self._server def onInit(self): - self.connect_button = self.getControl(CONNECT) self.cancel_button = self.getControl(CANCEL) self.error_toggle = self.getControl(ERROR_TOGGLE) self.error_msg = self.getControl(ERROR_MSG) self.host_field = self._add_editcontrol(755, 433, 40, 415) self.port_field = self._add_editcontrol(755, 543, 40, 415) - self.port_field.setText('8096') self.setFocus(self.host_field) - self.host_field.controlUp(self.cancel_button) self.host_field.controlDown(self.port_field) self.port_field.controlUp(self.host_field) @@ -70,28 +59,24 @@ def onInit(self): self.cancel_button.controlDown(self.host_field) def onClick(self, control): - if control == CONNECT: # Sign in to emby connect self._disable_error() - server = self.host_field.getText() port = self.port_field.getText() if not server: # Display error - self._error(ERROR['Empty'], _('empty_server')) - LOG.error("Server cannot be null") + self._error(ERROR['Empty'], helper.translate._('empty_server')) + self.LOG.error("Server cannot be null") elif self._connect_to_server(server, port): self.close() - + #Remind me later elif control == CANCEL: - # Remind me later self.close() def onAction(self, action): - if self.error == ERROR['Empty'] and self.host_field.getText() and self.port_field.getText(): self._disable_error() @@ -99,47 +84,35 @@ def onAction(self, action): self.close() def _add_editcontrol(self, x, y, height, width): - - media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') - control = xbmcgui.ControlEdit(0, 0, 0, 0, - label="User", - font="font13", - textColor="FF52b54b", - disabledColor="FF888888", - focusTexture="-", - noFocusTexture="-") +# media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlEdit(0, 0, 0, 0, label="", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-") control.setPosition(x, y) control.setHeight(height) control.setWidth(width) - self.addControl(control) return control def _connect_to_server(self, server, port): - server_address = "%s:%s" % (server, port) if port else server - self._message("%s %s..." % (_(30610), server_address)) + self._message("%s %s..." % (helper.translate._(30610), server_address)) result = self.connect_manager['manual-server'](server_address) - if result['State'] == CONNECTION_STATE['Unavailable']: - self._message(_(30609)) + if result['State'] == emby.core.connection_manager.CONNECTION_STATE['Unavailable']: + self._message(helper.translate._(30609)) return False - else: - self._server = result['Servers'][0] - return True - def _message(self, message): + self._server = result['Servers'][0] + return True + def _message(self, message): self.error_msg.setLabel(message) self.error_toggle.setVisibleCondition('true') def _error(self, state, message): - self.error = state self.error_msg.setLabel(message) self.error_toggle.setVisibleCondition('true') def _disable_error(self): - self.error = None self.error_toggle.setVisibleCondition('false') diff --git a/resources/lib/dialogs/usersconnect.py b/dialogs/usersconnect.py similarity index 65% rename from resources/lib/dialogs/usersconnect.py rename to dialogs/usersconnect.py index 5c4a55b07..36175ed5f 100644 --- a/resources/lib/dialogs/usersconnect.py +++ b/dialogs/usersconnect.py @@ -1,15 +1,7 @@ # -*- coding: utf-8 -*- - -################################################################################################## - import logging - -import xbmc import xbmcgui -################################################################################################## - -LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -19,27 +11,21 @@ MANUAL = 200 CANCEL = 201 -################################################################################################## - - class UsersConnect(xbmcgui.WindowXMLDialog): - - _user = None - _manual_login = False - - def __init__(self, *args, **kwargs): - - self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + self._user = None + self._manual_login = False + self.list_ = None + self.LOG = logging.getLogger("EMBY.dialogs.userconnect.UsersConnect") xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + #connect_manager, user_image, servers, emby_connect def set_args(self, **kwargs): - # connect_manager, user_image, servers, emby_connect - for key, value in kwargs.iteritems(): + for key, value in list(kwargs.items()): setattr(self, key, value) def is_user_selected(self): - return True if self._user else False + return bool(self._user) def get_user(self): return self._user @@ -47,39 +33,30 @@ def get_user(self): def is_manual_login(self): return self._manual_login - def onInit(self): - self.list_ = self.getControl(LIST) + for user in self.users: - user_image = ("items/logindefault.png" if 'PrimaryImageTag' not in user - else self._get_user_artwork(user['Id'], 'Primary')) + user_image = ("items/logindefault.png" if 'PrimaryImageTag' not in user else self._get_user_artwork(user['Id'], 'Primary')) self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image)) self.setFocus(self.list_) def _add_listitem(self, label, user_id, user_image): - item = xbmcgui.ListItem(label) item.setProperty('id', user_id) - if self.kodi_version > 15: - item.setArt({'Icon': user_image}) - else: - item.setIconImage(user_image) - + item.setArt({'Icon': user_image}) return item def onAction(self, action): - if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): self.close() if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): - if self.getFocusId() == LIST: user = self.list_.getSelectedItem() selected_id = user.getProperty('id') - LOG.info('User Id selected: %s', selected_id) + self.LOG.info('User Id selected: %s', selected_id) for user in self.users: if user['Id'] == selected_id: @@ -89,14 +66,12 @@ def onAction(self, action): self.close() def onClick(self, control): - if control == MANUAL: self._manual_login = True self.close() - elif control == CANCEL: self.close() + #Load user information set by UserClient def _get_user_artwork(self, user_id, item_type): - # Load user information set by UserClient return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, user_id, item_type) diff --git a/donations.png b/donations.png deleted file mode 100644 index fd5c0a4a7b4b40052a50670ef659fb50f9737b8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9087 zcmb_?WmME(&_4=DNhu+XsI(w0T}vZf(xNP#DC}dQkI7Q z`tJYo@j1_nd%ov<&pi`&?tEtE&V*~IDG=gQ<6~f85GpAGv@kFp8UJ0lkI{b!zQ%i_ zKX}fHA3+!xL_L4kqht;u8Vn3Z3?+cHj%Oxv`6GySHjVlB(HDogN6(BOaXo8@%U4uN z`qDC=;NWGif|{FL9$y~k5hvvX3Gu+rWVdI)xpd>t^Wta|96nOKQBG=@=V#M=)=S8U zYr%p?f`wmNZVVTqY|i3_VCY3|tv|Ywkd_Xvs3@ohrzhLgdoJgXk{eB zkdk7>`0qarw9I)J7>{KhVPFM4#=ynJ#vmcV$6);5Z@AJEkJl>KRc~112O*>Md?k== zY6_wc#g*U8F6mdqlm3Nsp5P~TG{0brgkbUNzB%{$$t(~mgAKjB}LHxNRt`Ax%Ju$m4rmk9q!rAMC` zI49tCvU7s;d09tvGok;^7`eIvM{x3EekPo`VC?kxC-!-jpBd~C4^A1t>%~PuU{@p3 z=1E5=LRRTt>~AQD>T0}4Hrh-#g#hh)PH-z-J3FB6GXF{>PxlV#fPk%*qbV!^7yC5q z0m$1Jhn~B~|0AnT1ulUgRLH&Y%XyZZV7quGrY+VYS_L*HW<$jJY%?MT5==0fgOHTH zlqDsFSR*fy)yfoCBa!~uXXgfQn~VwIy6wWT;!L5erJPXJm^R*DC*bTGHIJ9->0yd| zI2~qXIsuG~`TBmSNzpd);SLDWr_$XKG~6QGz07l;88M()sL01rlq!4{jQfCEGud_p zS-R+VL-A+ti~}Q%^Z`9ReG5eK%8RXzaRc%kZ7ni0LClOmwZa> zK`dG4zyUJ;gS(pU)7w~Dwmd;_JsuT5l0 zg$woSvrjK;5wT5OLH(d?)A578;Be~)0YTBFwq4cN*RhXbjlAM|?ZO`_!gbUo9)@IF zRbmp806i!JJ`%rF@5)oTjAwjMGr%MpA#Xb^hh~ ziZ;Sg&WXw%t%k~(oB26;go`Q(m1sFB`$<>kz~}6X^Pm#$)~-~Dm9Od3rjZI^vAIyY zGWldm!kGO&rAO1>DfO74mmDgC5k{>dezjH}#%?d^pG_MDy&*AvF++`u%~3JwbL@~4 zG*GHs$^*4EeEO$wjB@#R1f)cq%Z{#4&M&`3+;0>aHYrn9D5F(F#7QlS?v zXmPE?Os8m_wO(kjJBn^r?>hC!bn6%LTPJDaYE}eJ&xcRXpS&l{zaH(7IoVon6$KH| z7*X~56Jh!n&{W=Xu2nITRs0Ys*S4M;W~>w2lj_-Xq|N5jkULA*w#n%i_Z^?s_ig`O z#~`nK`>w!Lok~1Gr#Lt6ZCg)KV3WAEw$IXqU2<|6M?>((kM$ZvTG2fC8cGIA}tDTVanJN5V=u(aq;al@)Rqdb`s}c zHe3d3N`VPq37gv}uux;c$zj@*FaAhjl9|cJ4)=K@!_rQ$w^e^^XegbUS>z-MlLa$y z;?su^tuSmmv?3%h@hRB6oQ@6|qXjDLJ3sdmv8}Vz67Ci;%H7&xMv+_=^&*}6hd13T z0NNmj#1mHvf{F9Qb!i}J4U@+S(94$J;4KX|_3{AQ`RSoa?4NVnk9|Cbrty!z%di?S ziCp(*WwJ?RvojI3a#Yx6&+@PDLnLCoeO~?U{m)K5IR8XPshN?kiA3+1X6q^McaWjY z3+@tYO@sNkW&gBhcf>cZ(^_{(Z85K5U8lkA;Ne=p#%mhJkPkg2lC+YGuXa?|E?)UYw*N!Onq#G})A{umOB6MZ^r_f3WJjsJ)^2v?@UY1U)CdfUmQsWj zlbgL<>6M4X*jOp3RLw6=WZ1+d>3?+NxL3dC6p`g;?i1@zq6xh!N9 zIZYg2Whv9t7`HAu_jom3-ND$#F*(7=PPiX|hxSsMNeG|*JZLi7;gUWT_2+A6wQt7w zEB}P(7^m7x@*N_SG^@c&yvLdoaMgyNO0A|7aU_xkMZOpbgNXowTwGGkb}O~!pKfpN zet@E-f7P!D<7%6hpv3G!aTmx|mq>NSY02`%;6@-Cg(0U(ro%D(JY{)NsC!;7W6_zp zIAP<~XST}U$DalsRei`@TI$bx;58nwAivm0YF}~m3HJ>)2NuV0GJ(}{nO$$530O$N*@`MHZdtZL?)Hj4w#!Iq;TrwW);mBl_$cFHmbyQxA0`V4(plEQ+ zh-Jz|zO0CVw2w#e(60(GqcYGul10KI27zlADWqRKVk@VX!)2prJ$LxS*fo;@xm44j z6U9M3YA4R4px-e`=J+2k1MMuoe2F|Rwh<%QfqBx_xoz{eEg1)3e$!|D{*J(42T@%v zbDhms2EeT89d7E3mbtjhAQrEq5OwA>p=-3XFZS_{Hk3bEbZX`=71I6FtF- zY%)ztL7v<~^S40(bg4?heLS7g+LAZfoz4Y5*9Eo@+Y{Y4D&53e-Zzsbh81)dS{g~F zxi4+Se=i`fPN$O1Erkyb;~Ldodpp%<76hL#4<&>x%ue3j{>~lm`caylL|3&*GfIJ< zI`6_O+qY(lj+CEfr5>h`TVIdXRHLO_V}6if-Tx#6yX1YlLd1k#P)wXm|6qbk%8{wr0~5(-S>Z>`9C0d(XT{+Rl>WZE0Ovm}I))lSefe zj`>8x2pi8K_~Cq=K=!VE?})Hi1$W0(3lNdSR2f**n_QQ(EO7$7f@rT)()5TdPupv-aF}WmH&{Quw6o+# z;fi)u6?zKrw|6JVEo}xptG;>l5pY7QjH~jqC zlkoeFk|)<}YlD}q$w~5r}GYI8nSU^%eLII2~7#JSTpZeq^%Bi0j3pise{7C zBi3r#{X%sHe?8eq9n*nmk%UpmB{yo$NY)tldnzs^xp?M2gm=)_y^ga{AaN?MN`8qh zB3`xKUn3?bCxh-)&|!a$sj83#NWYJ~J6gSh!&eUL@m`wRRVpap+*?0HX#Y54J1!tk zV$$`9iCcQ3_49p(q9{w*z0aD$B8*J21~`%KHb+kd3$lgZs&Me9B`_;hp6i?Wo?j!N zV`F2`WM*flm*}*a&??5A*&^R6B;LA-o)M zcYlkWb&zxFW5!SNrS&B#sdlCO$QL3ml5i53GXK-`!mGeNXYj*)wj)q48W?Chwa8{l z?*Z@dJeiVGdR+H{i*ki3d5d)cO))sm|qP z+1M!_;e&!`wZ@44NG?s6b*F2{+3RTRHig8q8djwh(DbCwpS9Iljdk3i7GaLd5jige zaP?$a4f3QXv&Q8-OJNtC5}+?u>QI;1QZRq^z$NA3j-mGG2L`Rq`}9S?8nZq7rWx&$ zF}}Lg{tchdH-bUJu`{(Sk#o4{QPS|1^UL)gerN=mLfY-oQ=ls%BFFok5@KR^w-Cr| z182GQGpy!!EnFFh8RTtW`Wl>poNFIl#II7l@N9zc*{d0YL}|*VZNXQ=W-$zsem5xO zu5LUojkFPhYy@-IF#T%9zQ^lkc6HmVQdIukQh-%x=&-=Ws^mjHHGhkiO7V4Eg4!?D zDjHqd)QfaCoV|xSBvA&z)e)hiYrKZ=?DX@@R@O zVn)lTSM01iMZP!N`g(rgy#z|@I%q;B|6u7K;+(O{_!&)1z{0)3?25)Ljfld&lOXZI z0c)SY6TpdT?BLVL?P6vtoCZVWRGIcs$4%AIO{4F|S_}!k-mmf8?E`pUUk^aFP_g~? z^0Orj)s+kp5&ue4nQ&UcbUk`8c)^+@q^DuyoO<#}twX-PEbd|Dcd?8oPt$JgsZFJS z^WC02@2qV@ti<(h-I&CgZ>JmsaXmS@{7CbSislKfbyd4aR(`nL%$s+;`gu-sqGRpW zjy2&z@+sM$dA2B~>WLM88dpLeRgmnTTd7s&GBV3ppq3H~=i<_0xp0m`tKJsIc)5<#p{f%-FSmNvD+J}sF-{foxw2X`N(;=@Th51Bu~TqM?AM9P~?uGh(K4Wo5;f>*6i_qCWfm^7EUW@57%XVIy}2 zPoc~os#FZMx%fgv?2KxT?ZHr4j}BYyW@8Dz+slCg+-GYNfs;5Kx2GwcVrb}@m`J!M zj^Q>3&a^euAKMgJM6)aTrOulkfkN1IQHbKSUoM#zQw(vdqO8+OE2zuY@Vla87qOw2lm~#?({v` zvS(VB?Sc<1|$RpDX^poBzJ0RYFfbw?xqZR zS*u>g7i-d+UMm9w0}XbwL<|x>n*;H_Hz)z{+wJ2USL@-%&c%mI#LifX$(Vua_&qeu zR(F3t<|v|5O4IC#uFk#z_(+Ni2KR_eb6RRTLEFTn{ado2;i*g_+G!O-#HMT;&^IVH zCWc=8+n>FXoRxYTZSByo%RZ2-sFQBFQ*4aqr&mR+B=4rn-C`A;4p}L(_x9WsN%^cI zpOHDP+}_@vZ1l4*Gr!16IWj{-V(N{70ptMuU*vuYOV}=xY3dVYX+0gn^^Va&;;++~p0~X=%b} zQ*Ca(&Wmz3la(uo{(0Tj+}sRX`G#niHKk!-xDeysqmHXEM}7HiO^dlRa5Y~LD_&!% z!KQ|~+Rb5zJ9=J3>7ium;!*as6rz$yloK?;!pt;bXx8a>x9qV>$i4dbLMVx>BJQcs z2xplNJ30kxaz`#Cs!Mns%+)w7HVSBt{|L;Yd-!@Bp3J?(5gDm@$ibvnD9|jx{+;(T zLj<7z`kUa6YZ@1x62=Er)wYNGo55ac!;_dUQ}n%m36hT!RxmIz5eIFLK%gDhC;bd5 z!&`0{Zwq94X5dw3V)vJ*Do5TOOZfYgi(Sz0kh4Xx^U zuVNDX0COM^=;(O3-Wxe4`Ea`tE4ea0K7O1xIMy%da2<+?bHCaQg4wy?SOHxjdsFtR z0CwnZ?|t^8{_%9a+2l#X*x1-kw0r~v1mNTgJUtnuF8o31_aJ^a=d59DbaeaGb_V#e zn*q(mXI03SycWd6s)Yly_p&+4PAEta!6|Qk&7wSS#4fQio;N8YzaVR4VS$d}&lQ7k z2+^-)<{zikqwxs_0jkrs=jafz1J5GJc#HNKdp-Ww#N$26;};hPW!PQeH4w-5rSl-R zW41%}<+9A9a}=tOIF_CUt7~_6cQBEqJ3Q58{a8v`sY_)0kt9`!J=fF9sw%(RlYaCG z?xQm`-7=kh4(gwFLz{FvIIS0A&2B37Ms^=g05&~h#v?~ZtijMUF;hF=t)XOM$y;X| zn=)S~nhO7f0P=kn1)?`w!Pnb(Os8T_-@aW(3$4txdK}e!?nfQWYjXmDs^(1l?3)k6 zpJSV)k4HznnDY&jTfD={x?MVEXBW4E5^HLidI8cT#!b#^=xc(*S+EAESul11QbsT}uMul>GQ6F8e zz6;jbN+mhu{ZcE{u2v~#%Ui>fySoth=mCz3Q+0B3s?FlameeW}TbZ2PV`ah*_m)kj?DSm953i6T|En#pu+;dt@0_MlNIwfQx-Crm<{{omh;{5f>^Or#rF%m z^o)Z5bS41LzB@e|lk_lwy>Z%fWF+JC*JuiCOeW)1PcDyE~*RT#JuhV`9W%$ooY1(PfOeuYAsbN z^+eGQKWhf2Q>(R>)qO0}^$eYD1kCShfM@Ai-%aHJ_(!m3-8wJVBg@LlN^<;Kw93$B z!b+xl!~GvDdR6qJPlZ`V9F;+qR|OTV5{sg(&L7+Wb9Z;t^*JJDWAp4vChzxqq5ur^ zPL40`%h!iWK>1$_0z`xDz@#ZC1yK@iz>kYNt+J%C4Lb}rADRm1e)ph|>4;Iujn6on z7#Nsu{>}ni84~!e5(ngJu#!dg8gqp*VQLV_=4F9?!x9~Cl)9^aoe%Q`2Y(wJ8aZe| zXM4RCH3po5l2D)F24q1a(#h7SXgj3sMU(@0O3y29^+tXQ?puq(K85G56Tc)-+Z>{OS&HftF z8-JLss)FdvB1{=lrSMA)-4R}fLWBQFpnA6hSw4Hj;;I^w)@ppjZeFnJ(t_#pD%&&InLM<0B!+>+BEQNpSy)THMKD|PzI3>Hs*m=f5 z(Q+*I+0CI0S=s(!BqX5L0q%v-1k?CC`|^J~L-PMmp