frontend and speed fixes
authormarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Mon, 4 Nov 2019 23:10:55 +0000 (00:10 +0100)
committermarcelveldt <marcelvanderveldt@MacBook-Pro.local>
Mon, 4 Nov 2019 23:10:55 +0000 (00:10 +0100)
15 files changed:
frontend/src/App.vue
frontend/src/components/ContextMenu.vue
frontend/src/components/InfoHeader.vue
frontend/src/components/ListviewItem.vue
frontend/src/components/PlayerOSD.vue
frontend/src/components/TopBar.vue
frontend/src/plugins/server.js
frontend/src/plugins/store.js
frontend/src/views/Browse.vue
frontend/src/views/ItemDetails.vue
frontend/src/views/PlayerQueue.vue
frontend/src/views/Search.vue
music_assistant/__init__.py
music_assistant/database.py
music_assistant/playerproviders/chromecast.py

index 471143c8195f8fb8cced0b738f19fbc6c51a378c..a3fd08aae93b48e8fcdf95e2304194ef4e2db58a 100644 (file)
@@ -1,9 +1,8 @@
 <template>
-  <v-app light>
+  <v-app>
     <TopBar />
     <NavigationMenu></NavigationMenu>
     <v-content>
-      <!-- <player></player> -->
       <router-view app :key="$route.path"></router-view>
     </v-content>
     <PlayerOSD :showPlayerSelect="showPlayerSelect" />
   </v-app>
 </template>
 
+<style>
+  .body {
+    background-color: black
+  }
+</style>
+
 <script>
 import Vue from 'vue'
 import NavigationMenu from './components/NavigationMenu.vue'
index 5784b2f37da1bd69d6152eb3a9087a02a3de5a93..1d9d999bb0d7464ab9675a49eab1302317ba0a7f 100644 (file)
@@ -8,7 +8,7 @@
         <div v-for="item of menuItems" :key="item.label">\r
           <v-list-item @click="itemCommand(item.action)">\r
             <v-list-item-avatar>\r
-              <v-icon>{{item.icon}}</v-icon>\r
+              <v-icon>{{ item.icon }}</v-icon>\r
             </v-list-item-avatar>\r
             <v-list-item-content>\r
               <v-list-item-title>{{ $t(item.label) }}</v-list-item-title>\r
@@ -31,7 +31,7 @@
           :hideproviders="false"\r
           :hidelibrary="true"\r
           :hidemenu="true"\r
-          @click="playlistSelected"\r
+          :onclickHandler="playlistSelected"\r
         ></listviewItem>\r
       </v-list>\r
     </v-card>\r
@@ -59,38 +59,6 @@ export default Vue.extend({
       subheader: '',\r
       curItem: null,\r
       curPlaylist: null,\r
-      mediaPlayItems: [\r
-        {\r
-          label: 'play_now',\r
-          action: 'play',\r
-          icon: 'play_circle_outline'\r
-        },\r
-        {\r
-          label: 'play_next',\r
-          action: 'next',\r
-          icon: 'queue_play_next'\r
-        },\r
-        {\r
-          label: 'add_queue',\r
-          action: 'add',\r
-          icon: 'playlist_add'\r
-        }\r
-      ],\r
-      showTrackInfoItem: {\r
-        label: 'show_info',\r
-        action: 'info',\r
-        icon: 'info'\r
-      },\r
-      addToPlaylistItem: {\r
-        label: 'add_playlist',\r
-        action: 'add_playlist',\r
-        icon: 'add_circle_outline'\r
-      },\r
-      removeFromPlaylistItem: {\r
-        label: 'remove_playlist',\r
-        action: 'remove_playlist',\r
-        icon: 'remove_circle_outline'\r
-      },\r
       playerQueueItems: [],\r
       playlists: []\r
     }\r
@@ -107,44 +75,128 @@ export default Vue.extend({
   computed: {\r
   },\r
   methods: {\r
-    showContextMenu (item, playlist = null) {\r
-      this.curItem = item\r
-      this.curPlaylist = playlist\r
-      if (!item) return\r
-      if (item.media_type === 3) {\r
-        // track item in list\r
-        let items = []\r
-        items.push(...this.mediaPlayItems)\r
-        items.push(this.showTrackInfoItem)\r
-        items.push(this.addToPlaylistItem)\r
-        if (!!playlist && playlist.is_editable) {\r
-          items.push(this.removeFromPlaylistItem)\r
+    showContextMenu (mediaItem) {\r
+      // show contextmenu items for the given mediaItem\r
+      this.playlists = []\r
+      if (!mediaItem) return\r
+      this.curItem = mediaItem\r
+      let curBrowseContext = this.$store.topBarContextItem\r
+      let menuItems = []\r
+      // show playmenu\r
+      menuItems.push({\r
+        label: 'play',\r
+        action: 'playmenu',\r
+        icon: 'play_circle_outline'\r
+      })\r
+      // show info\r
+      if (mediaItem !== curBrowseContext) {\r
+        menuItems.push({\r
+          label: 'show_info',\r
+          action: 'info',\r
+          icon: 'info'\r
+        })\r
+      }\r
+      // add to library\r
+      if (mediaItem.in_library.length === 0) {\r
+        menuItems.push({\r
+          label: 'add_library',\r
+          action: 'add_library',\r
+          icon: 'favorite_border'\r
+        })\r
+      }\r
+      // remove from library\r
+      if (mediaItem.in_library.length > 0) {\r
+        menuItems.push({\r
+          label: 'remove_library',\r
+          action: 'remove_library',\r
+          icon: 'favorite'\r
+        })\r
+      }\r
+      // remove from playlist (playlist tracks only)\r
+      if (curBrowseContext && curBrowseContext.media_type === 4) {\r
+        this.curPlaylist = curBrowseContext\r
+        if (mediaItem.media_type === 3 && curBrowseContext.is_editable) {\r
+          menuItems.push({\r
+            label: 'remove_playlist',\r
+            action: 'remove_playlist',\r
+            icon: 'remove_circle_outline'\r
+          })\r
         }\r
-        this.menuItems = items\r
-      } else {\r
-        // all other playable media\r
-        this.menuItems = this.mediaPlayItems\r
       }\r
-      this.header = item.name\r
+      // add to playlist action (tracks only)\r
+      if (mediaItem.media_type === 3) {\r
+        menuItems.push({\r
+          label: 'add_playlist',\r
+          action: 'add_playlist',\r
+          icon: 'add_circle_outline'\r
+        })\r
+      }\r
+      this.menuItems = menuItems\r
+      this.header = mediaItem.name\r
       this.subheader = ''\r
       this.visible = true\r
     },\r
-    showPlayMenu (item) {\r
-      this.curItem = item\r
-      if (!item) return\r
-      this.menuItems = this.mediaPlayItems\r
-      this.header = item.name\r
+    showPlayMenu (mediaItem) {\r
+      // show playmenu items for the given mediaItem\r
+      this.playlists = []\r
+      this.curItem = mediaItem\r
+      if (!mediaItem) return\r
+      let menuItems = [\r
+        {\r
+          label: 'play_now',\r
+          action: 'play',\r
+          icon: 'play_circle_outline'\r
+        },\r
+        {\r
+          label: 'play_next',\r
+          action: 'next',\r
+          icon: 'queue_play_next'\r
+        },\r
+        {\r
+          label: 'add_queue',\r
+          action: 'add',\r
+          icon: 'playlist_add'\r
+        }\r
+      ]\r
+      this.menuItems = menuItems\r
+      this.header = mediaItem.name\r
       this.subheader = ''\r
       this.visible = true\r
     },\r
+    async showPlaylistsMenu () {\r
+      // get all editable playlists\r
+      let trackProviders = []\r
+      for (let item of this.curItem.provider_ids) {\r
+        trackProviders.push(item.provider)\r
+      }\r
+      let playlists = await this.$server.getData('playlists')\r
+      let items = []\r
+      for (var playlist of playlists) {\r
+        if (\r
+          playlist.is_editable &&\r
+          (!this.curPlaylist || playlist.item_id !== this.curPlaylist.item_id)\r
+        ) {\r
+          for (let item of playlist.provider_ids) {\r
+            if (trackProviders.includes(item.provider)) {\r
+              items.push(playlist)\r
+              break\r
+            }\r
+          }\r
+        }\r
+      }\r
+      this.playlists = items\r
+    },\r
     itemCommand (cmd) {\r
       if (cmd === 'info') {\r
-        // show track info\r
+        // show media info\r
         this.$router.push({\r
-          path: '/tracks/' + this.curItem.item_id,\r
+          path: '/' + this.curItem.media_type + '/' + this.curItem.item_id,\r
           query: { provider: this.curItem.provider }\r
         })\r
         this.visible = false\r
+      } else if (cmd === 'playmenu') {\r
+        // show play menu\r
+        return this.showPlayMenu(this.curItem)\r
       } else if (cmd === 'add_playlist') {\r
         // add to playlist\r
         return this.showPlaylistsMenu()\r
@@ -165,50 +217,26 @@ export default Vue.extend({
     playlistSelected (playlistobj) {\r
       this.playlistAddRemove(\r
         this.curItem,\r
-        playlistobj,\r
+        playlistobj.item_id,\r
         'playlist_add'\r
       )\r
       this.visible = false\r
     },\r
-    playlistAddRemove (track, playlist, action = 'playlist_add') {\r
+    playlistAddRemove (track, playlistId, action = 'playlist_add') {\r
       /// add or remove track on playlist\r
-      var url = `${this.$store.server}api/track/${track.item_id}`\r
-      this.$axios\r
-        .get(url, {\r
-          params: {\r
-            provider: track.provider,\r
-            action: action,\r
-            action_details: playlist.item_id\r
-          }\r
-        })\r
+      let endpoint = 'track/' + track.item_id\r
+      let params = {\r
+        provider: track.provider,\r
+        action: action,\r
+        action_details: playlistId\r
+      }\r
+      this.$server.getData(endpoint, params)\r
         .then(result => {\r
           // reload listing\r
-          if (action === 'playlist_remove') this.$router.go()\r
-        })\r
-    },\r
-    async showPlaylistsMenu () {\r
-      // get all editable playlists\r
-      const url = this.$store.apiAddress + 'playlists'\r
-      let trackProviders = []\r
-      for (let item of this.curItem.provider_ids) {\r
-        trackProviders.push(item.provider)\r
-      }\r
-      let result = await this.$axios.get(url, {})\r
-      let items = []\r
-      for (var playlist of result.data) {\r
-        if (\r
-          playlist.is_editable &&\r
-          playlist.item_id !== this.curPlaylist.item_id\r
-        ) {\r
-          for (let item of playlist.provider_ids) {\r
-            if (trackProviders.includes(item.provider)) {\r
-              items.push(playlist)\r
-              break\r
-            }\r
+          if (action === 'playlist_remove') {\r
+            this.$server.$emit('refresh_listing')\r
           }\r
-        }\r
-      }\r
-      this.playlists = items\r
+        })\r
     }\r
   }\r
 })\r
index fc30fbe632756fd083fd94092321f413fbe9fc49..e7b11e172253abe0400f92042b6a005566162cd0 100644 (file)
@@ -2,7 +2,7 @@
   <v-flex v-observe-visibility="visibilityChanged">\r
     <v-card\r
       tile\r
-      color="cyan darken-2"\r
+      color="black"\r
       class="white--text"\r
       :img="require('../assets/info_gradient.jpg')"\r
       style="margin-top:-60px;"\r
@@ -13,7 +13,7 @@
         height="300"\r
         position="center top"\r
         :src="$server.getImageUrl(itemDetails, 'fanart')"\r
-        gradient="to bottom, rgba(66,66,66,.95), rgba(0,0,0,.75)"\r
+        gradient="to bottom, rgba(0,0,0,.90), rgba(0,0,0,.75)"\r
       >\r
         <div class="text-xs-center" style="height:40px;" id="whitespace_top" />\r
 \r
 \r
           <v-flex>\r
             <!-- Main title -->\r
-            <v-card-title style="text-shadow: 1px 1px #000000;" class="headline">\r
+            <v-card-title\r
+              style="text-shadow: 1px 1px #000000;"\r
+              class="headline"\r
+            >\r
               {{ itemDetails.name }}\r
             </v-card-title>\r
 \r
             <!-- other details -->\r
             <v-card-subtitle>\r
-\r
               <!-- version -->\r
-              <div v-if="itemDetails.version" class="caption"\r
-              style="color: white;"\r
-            >{{ itemDetails.version }}\r
+              <div\r
+                v-if="itemDetails.version"\r
+                class="caption"\r
+                style="color: white;"\r
+              >\r
+                {{ itemDetails.version }}\r
               </div>\r
 \r
-            <!-- item artists -->\r
-            <div class="title" style="text-shadow: 1px 1px #000000;" v-if="itemDetails.artists">\r
-                <v-icon color="#cccccc" style="margin-left: -3px;margin-right:3px" small>person</v-icon>\r
+              <!-- item artists -->\r
+              <div\r
+                class="title"\r
+                style="text-shadow: 1px 1px #000000;"\r
+                v-if="itemDetails.artists"\r
+              >\r
+                <v-icon\r
+                  color="#cccccc"\r
+                  style="margin-left: -3px;margin-right:3px"\r
+                  small\r
+                  >person</v-icon\r
+                >\r
                 <span\r
                   v-for="(artist, artistindex) in itemDetails.artists"\r
                   :key="artist.db_id"\r
                 >\r
-                  <a style="color: primary" v-on:click="artistClick(artist)">{{ artist.name }}</a>\r
+                  <a style="color: primary" v-on:click="artistClick(artist)">{{\r
+                    artist.name\r
+                  }}</a>\r
                   <span\r
                     style="color: #cccccc"\r
                     v-if="artistindex + 1 < itemDetails.artists.length"\r
                     :key="artistindex"\r
-                  >{{ ' / ' }}</span>\r
+                    >{{ " / " }}</span\r
+                  >\r
                 </span>\r
-            </div>\r
+              </div>\r
 \r
-            <!-- album artist -->\r
-            <div class="title" v-if="itemDetails.artist">\r
-                <v-icon color="#cccccc" style="margin-left: -3px;margin-right:3px" small>person</v-icon>\r
+              <!-- album artist -->\r
+              <div class="title" v-if="itemDetails.artist">\r
+                <v-icon\r
+                  color="#cccccc"\r
+                  style="margin-left: -3px;margin-right:3px"\r
+                  small\r
+                  >person</v-icon\r
+                >\r
                 <a\r
                   style="color: primary"\r
                   v-on:click="artistClick(itemDetails.artist)"\r
-                >{{ itemDetails.artist.name }}</a>\r
-            </div>\r
+                  >{{ itemDetails.artist.name }}</a\r
+                >\r
+              </div>\r
 \r
-            <!-- playlist owner -->\r
-            <div class="title" style="text-shadow: 1px 1px #000000;" v-if="itemDetails.owner">\r
-                <v-icon color="#cccccc" style="margin-left: -3px;margin-right:3px" small>person</v-icon>\r
+              <!-- playlist owner -->\r
+              <div\r
+                class="title"\r
+                style="text-shadow: 1px 1px #000000;"\r
+                v-if="itemDetails.owner"\r
+              >\r
+                <v-icon\r
+                  color="#cccccc"\r
+                  style="margin-left: -3px;margin-right:3px"\r
+                  small\r
+                  >person</v-icon\r
+                >\r
                 <a style="color:primary">{{ itemDetails.owner }}</a>\r
-            </div>\r
+              </div>\r
 \r
-            <div\r
-              v-if="itemDetails.album"\r
-              style="color:#ffffff;text-shadow: 1px 1px #000000;"\r
-            >\r
-              <v-icon color="#cccccc" style="margin-left: -3px;margin-right:3px" small>album</v-icon>\r
-              <a\r
-                style="color:#ffffff"\r
-                v-on:click="albumClick(itemDetails.album)"\r
-              >{{ itemDetails.album.name }}</a>\r
-            </div>\r
+              <div\r
+                v-if="itemDetails.album"\r
+                style="color:#ffffff;text-shadow: 1px 1px #000000;"\r
+              >\r
+                <v-icon\r
+                  color="#cccccc"\r
+                  style="margin-left: -3px;margin-right:3px"\r
+                  small\r
+                  >album</v-icon\r
+                >\r
+                <a\r
+                  style="color:#ffffff"\r
+                  v-on:click="albumClick(itemDetails.album)"\r
+                  >{{ itemDetails.album.name }}</a\r
+                >\r
+              </div>\r
             </v-card-subtitle>\r
 \r
             <!-- play/info buttons -->\r
             <div style="margin-left:14px;">\r
-              <v-btn color="primary" tile\r
+              <v-btn\r
+                color="primary"\r
+                tile\r
                 @click="$server.$emit('showPlayMenu', itemDetails)"\r
               >\r
-              <v-icon left dark>play_circle_filled</v-icon>\r
-                {{ $t('play') }}\r
+                <v-icon left dark>play_circle_filled</v-icon>\r
+                {{ $t("play") }}\r
               </v-btn>\r
-              <v-btn style="margin-left:10px;"\r
-                v-if="!$store.isMobile && !!itemDetails.in_library && itemDetails.in_library.length == 0"\r
-                color="primary" tile\r
+              <v-btn\r
+                style="margin-left:10px;"\r
+                v-if="\r
+                  !$store.isMobile &&\r
+                    !!itemDetails.in_library &&\r
+                    itemDetails.in_library.length == 0\r
+                "\r
+                color="primary"\r
+                tile\r
                 @click="toggleLibrary(itemDetails)"\r
               >\r
                 <v-icon left dark>favorite_border</v-icon>\r
-                  {{ $t('add_library') }}\r
+                {{ $t("add_library") }}\r
               </v-btn>\r
-              <v-btn style="margin-left:10px;"\r
-                v-if="!$store.isMobile && !!itemDetails.in_library && itemDetails.in_library.length > 0"\r
-                color="primary" tile\r
+              <v-btn\r
+                style="margin-left:10px;"\r
+                v-if="\r
+                  !$store.isMobile &&\r
+                    !!itemDetails.in_library &&\r
+                    itemDetails.in_library.length > 0\r
+                "\r
+                color="primary"\r
+                tile\r
                 @click="toggleLibrary(itemDetails)"\r
               >\r
                 <v-icon left dark>favorite</v-icon>\r
-                  {{ $t('remove_library') }}\r
+                {{ $t("remove_library") }}\r
               </v-btn>\r
             </div>\r
 \r
             <!-- Description/metadata -->\r
             <v-card-subtitle class="body-2">\r
               <div class="justify-left" style="text-shadow: 1px 1px #000000;">\r
-                <ReadMore :text="getDescription()" :max-chars="$store.isMobile ? 100 : 300" />\r
+                <ReadMore\r
+                  :text="getDescription()"\r
+                  :max-chars="$store.isMobile ? 100 : 300"\r
+                />\r
               </div>\r
               <!-- tech specs and provider icons -->\r
-            <ProviderIcons\r
-              v-bind:providerIds="itemDetails.provider_ids"\r
-              :height="25"\r
-              :dark="true"\r
-              v-if="$store.isMobile"\r
-            />\r
+              <ProviderIcons\r
+                v-bind:providerIds="itemDetails.provider_ids"\r
+                :height="25"\r
+                :dark="true"\r
+                v-if="$store.isMobile"\r
+              />\r
             </v-card-subtitle>\r
           </v-flex>\r
         </v-layout>\r
@@ -162,15 +217,24 @@ export default Vue.extend({
   },\r
   mounted () { },\r
   created () {\r
-    this.$store.topBarColor = 'transparent'\r
+    this.$store.topBarTransparent = true\r
   },\r
   beforeDestroy () {\r
-    this.$store.topBarColor = '#424242'\r
+    this.$store.topBarTransparent = false\r
+    this.$store.topBarContextItem = null\r
+  },\r
+  watch: {\r
+    itemDetails: function (val) {\r
+      // set itemDetails as contextitem\r
+      if (val) {\r
+        this.$store.topBarContextItem = val\r
+      }\r
+    }\r
   },\r
   methods: {\r
     visibilityChanged (isVisible, entry) {\r
-      if (isVisible) this.$store.topBarColor = 'transparent'\r
-      else this.$store.topBarColor = '#424242'\r
+      if (isVisible) this.$store.topBarTransparent = true\r
+      else this.$store.topBarTransparent = false\r
     },\r
     artistClick (item) {\r
       // artist entry clicked\r
index b8c45f8c22aaf94f60c9395b108629401e21f684..eec96b11da101d29e30affcffadabf8fedf63933 100644 (file)
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <v-list-item ripple @click="$emit('click', item)">
+    <v-list-item ripple @click="itemClicked(item)">
       <v-list-item-avatar tile color="grey" v-if="!hideavatar">
         <img
           :src="$server.getImageUrl(item, 'image', 80)"
 
         <v-list-item-subtitle v-if="item.artists">
           <span v-for="(artist, artistindex) in item.artists" :key="artist.item_id">
-            <a v-on:click="artistClick(artist)" @click.stop>{{ artist.name }}</a>
+            <a v-on:click="itemClicked(artist)" @click.stop>{{ artist.name }}</a>
             <label v-if="artistindex + 1 < item.artists.length" :key="artistindex">/</label>
           </span>
           <a
             v-if="!!item.album && !!hidetracknum"
-            v-on:click="albumClick(item.album)"
+            v-on:click="itemClicked(item.album)"
             @click.stop
             style="color:grey"
           > - {{ item.album.name }}</a>
@@ -32,7 +32,7 @@
           >- disc {{ item.disc_number }} track {{ item.track_number }}</label>
         </v-list-item-subtitle>
         <v-list-item-subtitle v-if="item.artist">
-          <a v-on:click="artistClick(item.artist)" @click.stop>{{ item.artist.name }}</a>
+          <a v-on:click="itemClicked(item.artist)" @click.stop>{{ item.artist.name }}</a>
         </v-list-item-subtitle>
 
         <v-list-item-subtitle v-if="!!item.owner">{{ item.owner }}</v-list-item-subtitle>
@@ -72,7 +72,7 @@
       <!-- menu button/icon -->
       <v-icon
         v-if="!hidemenu"
-        @click="$emit('menuClick', item)"
+        @click="menuClick(item)"
         @click.stop
         color="grey lighten-1"
         style="margin-right:-10px;padding-left:10px"
@@ -99,7 +99,8 @@ export default Vue.extend({
     hideproviders: Boolean,
     hidemenu: Boolean,
     hidelibrary: Boolean,
-    hideduration: Boolean
+    hideduration: Boolean,
+    onclickHandler: null
   },
   data () {
     return {}
@@ -116,19 +117,30 @@ export default Vue.extend({
   },
   mounted () { },
   methods: {
-    artistClick (item) {
-      // artist entry clicked within the listviewItem
-      var url = '/artists/' + item.item_id
-      this.$router.push({ path: url, query: { provider: item.provider } })
+    itemClicked (mediaItem) {
+      // mediaItem in the list is clicked
+      if (this.onclickHandler) return this.onclickHandler(mediaItem)
+      let url = ''
+      if (mediaItem.media_type === 1) {
+        url = '/artists/' + mediaItem.item_id
+      } else if (mediaItem.media_type === 2) {
+        url = '/albums/' + mediaItem.item_id
+      } else if (mediaItem.media_type === 4) {
+        url = '/playlists/' + mediaItem.item_id
+      } else {
+        // assume track (or radio) item
+        this.$server.$emit('showPlayMenu', mediaItem)
+        return
+      }
+      this.$router.push({ path: url, query: { provider: mediaItem.provider } })
     },
-    albumClick (item) {
-      // album entry clicked within the listviewItem
-      var url = '/albums/' + item.item_id
-      this.$router.push({ path: url, query: { provider: item.provider } })
+    menuClick (mediaItem) {
+      // contextmenu button clicked
+      this.$server.$emit('showContextMenu', mediaItem)
     },
-    toggleLibrary (item) {
+    toggleLibrary (mediaItem) {
       // library button clicked on item
-      this.$server.toggleLibrary(item)
+      this.$server.toggleLibrary(mediaItem)
     }
   }
 })
index c16761c97d5598930b5bcf53ddd348cc2723c499..b3eb30a94297f36b25368c8ae061725402fab37c 100644 (file)
@@ -5,7 +5,7 @@
     padless
     light
     elevation="10"
-    style="background-color: #424242"
+    style="background-color: black;"
   >
     <v-card
       dense
@@ -89,7 +89,7 @@
       <v-list-item
         dark
         dense
-        style="height:44px;margin-bottom:5px;margin-top:-4px;background-color:#424242;"
+        style="height:44px;margin-bottom:5px;margin-top:-4px;background-color:black;"
       >
         <v-list-item-action v-if="$server.activePlayer" style="margin-top:15px">
           <v-btn small icon @click="playerCommand('previous')">
         </v-list-item-action>
       </v-list-item>
       <!-- add some additional whitespace in standalone mode only -->
-      <div style="height:14px" v-if="$store.isInStandaloneMode" />
-
+      <v-card
+        dense
+        flat
+        light
+        subheader
+        tile
+        width="100%"
+        color="black"
+        style="height:20px" v-if="$store.isInStandaloneMode"/>
   </v-footer>
 </template>
 
index 4b7e8d0e4d2bc10c5a0771bf27901d29f5421472..bc824a5668fcec111d379e66138b2bbb77ce1928 100644 (file)
@@ -1,7 +1,7 @@
 <template>
-  <v-app-bar app scroll-off-screen flat dense dark :color="$store.topBarColor">
+  <v-app-bar app flat dense dark :color="color">
     <v-layout>
-      <v-toolbar-title class="body-1" v-if="$store.topBarColor != 'transparent'" style="position:fixed;width:100%;text-align:center;vertical-align:center;margin-top:11px;">{{ $store.windowtitle }}</v-toolbar-title>
+      <div class="body-1" v-if="!$store.topBarTransparent" style="position:fixed;width:100%;text-align:center;vertical-align:center;margin-top:11px;">{{ $store.windowtitle }}</div>
       <v-btn icon v-on:click="$store.showNavigationMenu=!$store.showNavigationMenu" style="margin-left:-13px">
         <v-icon>menu</v-icon>
       </v-btn>
@@ -9,7 +9,7 @@
         <v-icon>arrow_back</v-icon>
       </v-btn>
       <v-spacer></v-spacer>
-      <v-btn icon @click="$server.$emit('showContextMenu')" style="margin-right:-23px">
+      <v-btn v-if="$store.topBarContextItem" icon @click="$server.$emit('showContextMenu', $store.topBarContextItem)" style="margin-right:-23px">
         <v-icon>more_vert</v-icon>
       </v-btn>
     </v-layout>
 import Vue from 'vue'
 
 export default Vue.extend({
-  props: {
-    'color': {
-      type: String,
-      default: '#424242'
-    },
-    'fixed': {
-      type: Boolean,
-      default: true
-    },
-    'context': {
-      type: Object,
-      default: null
-    }
-  },
+  props: { },
   data () {
     return {
     }
   },
+  computed: {
+    color () {
+      if (this.$store.topBarTransparent) {
+        return 'transparent'
+      } else return 'black'
+    }
+  },
   mounted () { },
   methods: {}
 })
index 0150a63023260f8a17412fde2bfaee5f68258910..6b9a8ae8e38233ad190042b6e0698c9457f93e43 100644 (file)
@@ -74,6 +74,29 @@ const server = new Vue({
       return result.data
     },
 
+    async getAllItems (endpoint, list, params = {}) {
+      // retrieve all items and fill list
+      var offset = 0
+      var limit = 50
+      var index = 0
+      while (true) {
+        let items = await this.$server.getData(endpoint, { offset: offset, limit: limit, ...params })
+        if (!items || items.length === 0) break
+        for (var item of items) {
+          if (list.length >= index) {
+            Vue.set(list, index, item)
+          } else list.push(item)
+          index += 1
+        }
+        offset += limit
+        if (items.length < limit) break
+      }
+      // truncate list if needed
+      if (list.length > index) {
+        list = list.slice(0, index)
+      }
+    },
+
     playerCommand (cmd, cmd_opt = null, playerId = this.activePlayerId) {
       let msgDetails = {
         player_id: playerId,
index 435e98bde03056f0969bd5f4d0a347642ba4959d..f0e6e182e0f4a38e9568f48561af1dfc9455b181 100644 (file)
@@ -6,7 +6,8 @@ const globalStore = new Vue({
       windowtitle: 'Home',
       loading: false,
       showNavigationMenu: false,
-      topBarColor: '#424242',
+      topBarTransparent: false,
+      topBarContextItem: null,
       isMobile: false,
       isInStandaloneMode: false
     }
@@ -21,7 +22,7 @@ const globalStore = new Vue({
   methods: {
     handleWindowOptions () {
       this.isMobile = (document.body.clientWidth < 700)
-      this.isInStandaloneMode = 'standalone' in window.navigator && window.navigator.standalone
+      this.isInStandaloneMode = (window.navigator.standalone === true) || (window.matchMedia('(display-mode: standalone)').matches)
     }
   }
 })
index d79ea30ecdb5317550c60ee7c6fa52eb4ab68498..e11e59032756748fa3f4a1b3d6eb17c29d0752b6 100644 (file)
@@ -17,8 +17,6 @@
           :hidelibrary="true"
           :hidemenu="item.media_type == 3 ? $store.isMobile : false"
           :hideduration="item.media_type == 5"
-          v-on:click="itemClicked"
-          v-on:menuClick="menuClick"
         ></ListviewItem>
       </RecycleScroller>
     </v-list>
@@ -49,37 +47,9 @@ export default {
     this.getItems()
   },
   methods: {
-    itemClicked (item) {
-      // item in the list is clicked
-      let url = ''
-      if (item.media_type === 1) {
-        url = '/artists/' + item.item_id
-      } else if (item.media_type === 2) {
-        url = '/albums/' + item.item_id
-      } else if (item.media_type === 4) {
-        url = '/playlists/' + item.item_id
-      } else {
-        // assume track (or radio) item
-        this.$server.$emit('showContextMenu', item)
-        return
-      }
-      this.$router.push({ path: url, query: { provider: item.provider } })
-    },
-    menuClick (item) {
-      // contextmenu button clicked
-      this.$server.$emit('showContextMenu', item)
-    },
     async getItems () {
       // retrieve the full list of items
-      let offset = 0
-      let limit = 50
-      while (true) {
-        let items = await this.$server.getData(this.mediatype, { offset: offset, limit: limit, provider: this.provider })
-        if (!items || items.length === 0) break
-        this.items.push(...items)
-        offset += limit
-        if (items.length < limit) break
-      }
+      return this.$server.getAllItems(this.mediatype, this.items, { provider: this.provider })
     }
   }
 }
index 120b1d4bf872103972bf185d2767b249ffea4def..27249b40b6bb8e0fbe8589b5e88f4b4ddbc40a0f 100644 (file)
@@ -2,14 +2,10 @@
   <section>
     <InfoHeader v-bind:itemDetails="itemDetails" />
     <v-tabs grow show-arrows v-model="activeTab">
-      <v-tab
-        v-for="tab in tabs"
-        :key="tab.label"
-      > {{ $t(tab.label) + ' (' + tab.items.length + ')' }}</v-tab>
-      <v-tab-item
-        v-for="tab in tabs"
-        :key="tab.label"
+      <v-tab v-for="tab in tabs" :key="tab.label">
+        {{ $t(tab.label) + " (" + tab.items.length + ")" }}</v-tab
       >
+      <v-tab-item v-for="tab in tabs" :key="tab.label">
         <v-card flat>
           <v-list two-line>
             <RecycleScroller
@@ -27,8 +23,6 @@
                 :hideproviders="$store.isMobile"
                 :hidelibrary="$store.isMobile"
                 :hidemenu="item.media_type == 3 ? $store.isMobile : false"
-                v-on:click="itemClicked"
-                v-on:menuClick="menuClick"
               ></ListviewItem>
             </RecycleScroller>
           </v-list>
@@ -67,9 +61,8 @@ export default {
       tabs: []
     }
   },
-  async created () {
-    // retrieve the item details
-    await this.getItemDetails()
+  created () {
+    this.$server.$on('refresh_listing', this.retrieveInfos)
     if (this.media_type === 'artists') {
       // artist details
       this.tabs = [
@@ -118,32 +111,19 @@ export default {
         }
       ]
     }
-    // retrieve the tabs with additional details
-    for (var tab of this.tabs) {
-      this.getTabItems(tab)
-    }
+    this.retrieveInfos()
+  },
+  beforeDestroy () {
+    this.$server.$off('refresh_listing')
   },
   methods: {
-    itemClicked (item) {
-      // listitem was clicked
-      // item in the list is clicked
-      let url = ''
-      if (item.media_type === 1) {
-        url = '/artists/' + item.item_id
-      } else if (item.media_type === 2) {
-        url = '/albums/' + item.item_id
-      } else if (item.media_type === 4) {
-        url = '/playlists/' + item.item_id
-      } else {
-        // assume track (or radio) item
-        this.$server.$emit('showContextMenu', item)
-        return
+    retrieveInfos () {
+      // retrieve the item details
+      this.getItemDetails()
+      // retrieve the tabs with additional details
+      for (var tab of this.tabs) {
+        this.getTabItems(tab)
       }
-      this.$router.push({ path: url, query: { provider: item.provider } })
-    },
-    menuClick (item) {
-      // contextmenu button (within listitem) clicked
-      this.$server.$emit('showContextMenu', item)
     },
     async getItemDetails () {
       // get the full details for the mediaitem
@@ -156,15 +136,11 @@ export default {
     },
     async getTabItems (tab) {
       // retrieve the lists of items for each tab
-      let offset = 0
-      let limit = 50
       let paginated = 'paginated' in tab ? tab.paginated : false
-      while (true) {
-        let items = await this.$server.getData(tab.endpoint, { offset: offset, limit: limit, provider: this.provider })
-        if (!items || items.length === 0) break
-        tab.items.push(...items)
-        offset += limit
-        if (items.length < limit || !paginated) break
+      if (paginated) {
+        return this.$server.getAllItems(tab.endpoint, tab.items, { provider: this.provider })
+      } else {
+        tab.items = await this.$server.getData(tab.endpoint, { provider: this.provider })
       }
     }
   }
index 0fae0e713bc0745bbcba4327fa7fabd663350c01..c091af28a0f901d4a411eb053c2ccddc8561dc90 100644 (file)
@@ -54,7 +54,6 @@
 </template>
 
 <script>
-import Vue from 'vue'
 import ListviewItem from '@/components/ListviewItem.vue'
 
 export default {
@@ -121,26 +120,8 @@ export default {
     },
     async getItems () {
       // retrieve the queue items
-      var offset = 0
-      var limit = 50
-      var index = 0
       const endpoint = 'players/' + this.$server.activePlayerId + '/queue'
-      while (true) {
-        let items = await this.$server.getData(endpoint, { offset: offset, limit: limit })
-        if (!items || items.length === 0) break
-        for (var item of items) {
-          if (this.items.length >= index) {
-            Vue.set(this.items, index, item)
-          } else this.items.push(item)
-          index += 1
-        }
-        offset += limit
-        if (items.length < limit) break
-      }
-      // truncate list if needed
-      if (this.items.length > index) {
-        this.items = this.items.slice(0, index + 1)
-      }
+      return this.$server.getAllItems(endpoint, this.items)
     }
   }
 }
index 5b0381c14c2febfe73e10813761e09649401bd46..0a98a604173e4a035c5e8d61fac9768e68dc8c4c 100644 (file)
@@ -28,8 +28,6 @@
               :hideproviders="$store.isMobile"
               :hideduration="$store.isMobile"
               :showlibrary="true"
-              v-on:click="itemClicked"
-              v-on:menuClick="menuClick"
             >
             </listviewItem>
           </v-list>
@@ -47,8 +45,6 @@
               v-bind:totalitems="artists.length"
               v-bind:index="index"
               :hideproviders="$store.isMobile"
-              v-on:click="itemClicked"
-          v-on:menuClick="menuClick"
             >
             </listviewItem>
           </v-list>
@@ -66,8 +62,6 @@
               v-bind:totalitems="albums.length"
               v-bind:index="index"
               :hideproviders="$store.isMobile"
-              v-on:click="itemClicked"
-              v-on:menuClick="menuClick"
             >
             </listviewItem>
           </v-list>
@@ -85,8 +79,6 @@
               v-bind:totalitems="playlists.length"
               v-bind:index="index"
               :hidelibrary="true"
-              v-on:click="itemClicked"
-              v-on:menuClick="menuClick"
             >
             </listviewItem>
           </v-list>
@@ -121,26 +113,6 @@ export default {
     this.$store.windowtitle = this.$t('search')
   },
   methods: {
-    itemClicked (item) {
-      // item in the list is clicked
-      let url = ''
-      if (item.media_type === 1) {
-        url = '/artists/' + item.item_id
-      } else if (item.media_type === 2) {
-        url = '/albums/' + item.item_id
-      } else if (item.media_type === 4) {
-        url = '/playlists/' + item.item_id
-      } else {
-        // assume track (or radio) item
-        this.$server.$emit('showContextMenu', item)
-        return
-      }
-      this.$router.push({ path: url, query: { provider: item.provider } })
-    },
-    menuClick (item) {
-      // contextmenu button clicked
-      this.$server.$emit('showContextMenu', item)
-    },
     async Search () {
       this.artists = []
       this.albums = []
index 92ac54418bcf4fd0d0f6836c2782b17b219b78f7..0a81b84ddbfa89f1fb6441f3ee9dbdc7d07418f5 100644 (file)
@@ -39,7 +39,7 @@ class MusicAssistant():
         self.event_listeners = {}
         self.config = MassConfig(self)
         # init modules
-        self.db = Database(datapath)
+        self.db = Database(self)
         self.cache = Cache(datapath)
         self.metadata = MetaData(self)
         self.web = Web(self)
@@ -58,20 +58,30 @@ class MusicAssistant():
         await self.players.setup()
         await self.web.setup()
         await self.http_streamer.setup()
+        # wait for exit
+        try:
+            while True:
+                await asyncio.sleep(3600)
+        except asyncio.CancelledError:
+            LOGGER.info("Application shutdown")
+            await self.signal_event("shutdown")
 
     def handle_exception(self, loop, context):
         ''' global exception handler '''
         LOGGER.debug(f"Caught exception: {context}")
         loop.default_exception_handler(context)
 
-    async def signal_event(self, msg, msg_details):
+    async def signal_event(self, msg, msg_details=None):
         ''' signal (systemwide) event '''
         if not (msg_details == None or isinstance(msg_details, (str, dict))):
             msg_details = serialize_values(msg_details)
         listeners = list(self.event_listeners.values())
         for callback, eventfilter in listeners:
             if not eventfilter or eventfilter in msg:
-                self.event_loop.create_task(callback(msg, msg_details))
+                if msg == 'shutdown':
+                    await callback(msg, msg_details)
+                else:
+                    self.event_loop.create_task(callback(msg, msg_details))
 
     async def add_event_listener(self, cb, eventfilter=None):
         ''' add callback to our event listeners '''
index 9b11d542b016c156c47e91daa550d3cbb90943e7..10117337a844bbb16dda571f0ad429ccfd837249 100755 (executable)
@@ -13,54 +13,60 @@ from .models.media_types import MediaType, Artist, Album, Track, Playlist, Radio
 
 class Database():
 
-    def __init__(self, datapath):
-        if not os.path.isdir(datapath):
-            raise FileNotFoundError(f"data directory {datapath} does not exist!")
-        self.dbfile = os.path.join(datapath, "database.db")
+    def __init__(self, mass):
+        self.mass = mass
+        if not os.path.isdir(mass.datapath):
+            raise FileNotFoundError(f"data directory {mass.datapath} does not exist!")
+        self._dbfile = os.path.join(mass.datapath, "database.db")
         logging.getLogger('aiosqlite').setLevel(logging.INFO)
 
+    async def on_shutdown(self, msg, msg_details):
+        ''' handle shutdown event, close db connection '''
+        await self._db.close()
+        LOGGER.info("db connection closed")
+
     async def setup(self):
         ''' init database '''
-        async with aiosqlite.connect(self.dbfile) as db:
-
-            await db.execute('CREATE TABLE IF NOT EXISTS library_items(item_id INTEGER NOT NULL, provider TEXT NOT NULL, media_type INTEGER NOT NULL, UNIQUE(item_id, provider, media_type));')
-
-            await db.execute('CREATE TABLE IF NOT EXISTS artists(artist_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, sort_name TEXT, musicbrainz_id TEXT NOT NULL UNIQUE);')            
-            await db.execute('CREATE TABLE IF NOT EXISTS albums(album_id INTEGER PRIMARY KEY AUTOINCREMENT, artist_id INTEGER NOT NULL, name TEXT NOT NULL, albumtype TEXT, year INTEGER, version TEXT, UNIQUE(artist_id, name, version, albumtype));')
-            await db.execute('CREATE TABLE IF NOT EXISTS labels(label_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);')
-            await db.execute('CREATE TABLE IF NOT EXISTS album_labels(album_id INTEGER, label_id INTEGER, UNIQUE(album_id, label_id));')
-
-            await db.execute('CREATE TABLE IF NOT EXISTS tracks(track_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, album_id INTEGER, duration INTEGER, version TEXT, disc_number INT, track_number INT, UNIQUE(name, album_id, version));')
-            await db.execute('CREATE TABLE IF NOT EXISTS track_artists(track_id INTEGER, artist_id INTEGER, UNIQUE(track_id, artist_id));')
-            
-            await db.execute('CREATE TABLE IF NOT EXISTS tags(tag_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);')
-            await db.execute('CREATE TABLE IF NOT EXISTS media_tags(item_id INTEGER, media_type INTEGER, tag_id, UNIQUE(item_id, media_type, tag_id));')
-            
-            await db.execute('CREATE TABLE IF NOT EXISTS provider_mappings(item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, prov_item_id TEXT NOT NULL, provider TEXT NOT NULL, quality INTEGER NOT NULL, details TEXT NULL, UNIQUE(item_id, media_type, prov_item_id, provider, quality));')
-            
-            await db.execute('CREATE TABLE IF NOT EXISTS metadata(item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, key TEXT NOT NULL, value TEXT, UNIQUE(item_id, media_type, key));')
-            await db.execute('CREATE TABLE IF NOT EXISTS external_ids(item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, key TEXT NOT NULL, value TEXT, UNIQUE(item_id, media_type, key, value));')
-            
-            await db.execute('CREATE TABLE IF NOT EXISTS playlists(playlist_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, owner TEXT NOT NULL, is_editable BOOLEAN NOT NULL, UNIQUE(name, owner));')
-            await db.execute('CREATE TABLE IF NOT EXISTS playlist_tracks(playlist_id INTEGER NOT NULL, track_id INTEGER NOT NULL, position INTEGER, UNIQUE(playlist_id, track_id));')
-            
-            await db.execute('CREATE TABLE IF NOT EXISTS radios(radio_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);')
-            
-            await db.execute('CREATE TABLE IF NOT EXISTS track_loudness(provider_track_id INTEGER NOT NULL, provider TEXT NOT NULL, loudness REAL, UNIQUE(provider_track_id, provider));')
-            
-            await db.commit()
-            await db.execute('VACUUM;')
+        self._db = await aiosqlite.connect(self._dbfile)
+        await self.mass.add_event_listener(self.on_shutdown, "shutdown")
+
+        await self._db.execute('CREATE TABLE IF NOT EXISTS library_items(item_id INTEGER NOT NULL, provider TEXT NOT NULL, media_type INTEGER NOT NULL, UNIQUE(item_id, provider, media_type));')
+
+        await self._db.execute('CREATE TABLE IF NOT EXISTS artists(artist_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, sort_name TEXT, musicbrainz_id TEXT NOT NULL UNIQUE);')            
+        await self._db.execute('CREATE TABLE IF NOT EXISTS albums(album_id INTEGER PRIMARY KEY AUTOINCREMENT, artist_id INTEGER NOT NULL, name TEXT NOT NULL, albumtype TEXT, year INTEGER, version TEXT, UNIQUE(artist_id, name, version, albumtype));')
+        await self._db.execute('CREATE TABLE IF NOT EXISTS labels(label_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);')
+        await self._db.execute('CREATE TABLE IF NOT EXISTS album_labels(album_id INTEGER, label_id INTEGER, UNIQUE(album_id, label_id));')
+
+        await self._db.execute('CREATE TABLE IF NOT EXISTS tracks(track_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, album_id INTEGER, duration INTEGER, version TEXT, disc_number INT, track_number INT, UNIQUE(name, album_id, version));')
+        await self._db.execute('CREATE TABLE IF NOT EXISTS track_artists(track_id INTEGER, artist_id INTEGER, UNIQUE(track_id, artist_id));')
+        
+        await self._db.execute('CREATE TABLE IF NOT EXISTS tags(tag_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);')
+        await self._db.execute('CREATE TABLE IF NOT EXISTS media_tags(item_id INTEGER, media_type INTEGER, tag_id, UNIQUE(item_id, media_type, tag_id));')
+        
+        await self._db.execute('CREATE TABLE IF NOT EXISTS provider_mappings(item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, prov_item_id TEXT NOT NULL, provider TEXT NOT NULL, quality INTEGER NOT NULL, details TEXT NULL, UNIQUE(item_id, media_type, prov_item_id, provider, quality));')
+        
+        await self._db.execute('CREATE TABLE IF NOT EXISTS metadata(item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, key TEXT NOT NULL, value TEXT, UNIQUE(item_id, media_type, key));')
+        await self._db.execute('CREATE TABLE IF NOT EXISTS external_ids(item_id INTEGER NOT NULL, media_type INTEGER NOT NULL, key TEXT NOT NULL, value TEXT, UNIQUE(item_id, media_type, key, value));')
+        
+        await self._db.execute('CREATE TABLE IF NOT EXISTS playlists(playlist_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, owner TEXT NOT NULL, is_editable BOOLEAN NOT NULL, UNIQUE(name, owner));')
+        await self._db.execute('CREATE TABLE IF NOT EXISTS playlist_tracks(playlist_id INTEGER NOT NULL, track_id INTEGER NOT NULL, position INTEGER, UNIQUE(playlist_id, track_id));')
+        
+        await self._db.execute('CREATE TABLE IF NOT EXISTS radios(radio_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);')
+        
+        await self._db.execute('CREATE TABLE IF NOT EXISTS track_loudness(provider_track_id INTEGER NOT NULL, provider TEXT NOT NULL, loudness REAL, UNIQUE(provider_track_id, provider));')
+        
+        await self._db.commit()
+        await self._db.execute('VACUUM;')
 
     async def get_database_id(self, provider:str, prov_item_id:str, media_type:MediaType):
         ''' get the database id for the given prov_id '''
-        async with aiosqlite.connect(self.dbfile) as db:
-            sql_query = 'SELECT item_id FROM provider_mappings WHERE prov_item_id = ? AND provider = ? AND media_type = ?;'
-            cursor = await db.execute(sql_query, (prov_item_id, provider, media_type))
-            item_id = await cursor.fetchone()
-            if item_id:
-                item_id = item_id[0]
-            await cursor.close()
-            return item_id
+        sql_query = 'SELECT item_id FROM provider_mappings WHERE prov_item_id = ? AND provider = ? AND media_type = ?;'
+        cursor = await self._db.execute(sql_query, (prov_item_id, provider, media_type))
+        item_id = await cursor.fetchone()
+        if item_id:
+            item_id = item_id[0]
+        await cursor.close()
+        return item_id
     
     async def search(self, searchquery, media_types:List[MediaType], limit=10):
         ''' search library for the given searchphrase '''
@@ -120,19 +126,18 @@ class Database():
         sql_query += ' ORDER BY %s' % orderby
         if limit:
             sql_query += ' LIMIT %d OFFSET %d' %(limit, offset)
-        async with aiosqlite.connect(self.dbfile) as db:
-            async with db.execute(sql_query) as cursor:
-                db_rows = await cursor.fetchall()
-            for db_row in db_rows:
-                playlist = Playlist()
-                playlist.item_id = db_row[0]
-                playlist.name = db_row[1]
-                playlist.owner = db_row[2]
-                playlist.is_editable = db_row[3]
-                playlist.metadata = await self.__get_metadata(playlist.item_id, MediaType.Playlist, db)
-                playlist.provider_ids = await self.__get_prov_ids(playlist.item_id, MediaType.Playlist, db)
-                playlist.in_library = await self.__get_library_providers(playlist.item_id, MediaType.Playlist, db)
-                playlists.append(playlist)
+        async with self._db.execute(sql_query) as cursor:
+            db_rows = await cursor.fetchall()
+        for db_row in db_rows:
+            playlist = Playlist()
+            playlist.item_id = db_row[0]
+            playlist.name = db_row[1]
+            playlist.owner = db_row[2]
+            playlist.is_editable = db_row[3]
+            playlist.metadata = await self.__get_metadata(playlist.item_id, MediaType.Playlist)
+            playlist.provider_ids = await self.__get_prov_ids(playlist.item_id, MediaType.Playlist)
+            playlist.in_library = await self.__get_library_providers(playlist.item_id, MediaType.Playlist)
+            playlists.append(playlist)
         return playlists
 
     async def radios(self, filter_query=None, provider=None, limit=100000, offset=0, orderby='name') -> List[Radio]:
@@ -146,17 +151,16 @@ class Database():
         sql_query += ' ORDER BY %s' % orderby
         if limit:
             sql_query += ' LIMIT %d OFFSET %d' %(limit, offset)
-        async with aiosqlite.connect(self.dbfile) as db:
-            async with db.execute(sql_query) as cursor:
-                db_rows = await cursor.fetchall()
-            for db_row in db_rows:
-                radio = Radio()
-                radio.item_id = db_row[0]
-                radio.name = db_row[1]
-                radio.metadata = await self.__get_metadata(radio.item_id, MediaType.Radio, db)
-                radio.provider_ids = await self.__get_prov_ids(radio.item_id, MediaType.Radio, db)
-                radio.in_library = await self.__get_library_providers(radio.item_id, MediaType.Radio, db)
-                items.append(radio)
+        async with self._db.execute(sql_query) as cursor:
+            db_rows = await cursor.fetchall()
+        for db_row in db_rows:
+            radio = Radio()
+            radio.item_id = db_row[0]
+            radio.name = db_row[1]
+            radio.metadata = await self.__get_metadata(radio.item_id, MediaType.Radio)
+            radio.provider_ids = await self.__get_prov_ids(radio.item_id, MediaType.Radio)
+            radio.in_library = await self.__get_library_providers(radio.item_id, MediaType.Radio)
+            items.append(radio)
         return items
 
     async def playlist(self, playlist_id:int) -> Playlist:
@@ -178,78 +182,74 @@ class Database():
     async def add_playlist(self, playlist:Playlist):
         ''' add a new playlist record into table'''
         assert(playlist.name)
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            async with db.execute('SELECT (playlist_id) FROM playlists WHERE name=? AND owner=?;', (playlist.name, playlist.owner)) as cursor:
-                result = await cursor.fetchone()
-                if result:
-                    playlist_id = result[0]
-                    # update existing
-                    sql_query = 'UPDATE playlists SET is_editable=? WHERE playlist_id=?;'
-                    await db.execute(sql_query, (playlist.is_editable, playlist_id))
-                else:
-                    # insert playlist
-                    sql_query = 'INSERT OR REPLACE INTO playlists (name, owner, is_editable) VALUES(?,?,?);'
-                    await db.execute(sql_query, (playlist.name, playlist.owner, playlist.is_editable))
-                    # get id from newly created item (the safe way)
-                    async with db.execute('SELECT (playlist_id) FROM playlists WHERE name=? AND owner=?;', (playlist.name,playlist.owner)) as cursor:
-                        playlist_id = await cursor.fetchone()
-                        playlist_id = playlist_id[0]
-                    LOGGER.debug('added playlist %s to database: %s' %(playlist.name, playlist_id))
+        async with self._db.execute('SELECT (playlist_id) FROM playlists WHERE name=? AND owner=?;', (playlist.name, playlist.owner)) as cursor:
+            result = await cursor.fetchone()
+            if result:
+                playlist_id = result[0]
+                # update existing
+                sql_query = 'UPDATE playlists SET is_editable=? WHERE playlist_id=?;'
+                await self._db.execute(sql_query, (playlist.is_editable, playlist_id))
+            else:
+                # insert playlist
+                sql_query = 'INSERT OR REPLACE INTO playlists (name, owner, is_editable) VALUES(?,?,?);'
+                await self._db.execute(sql_query, (playlist.name, playlist.owner, playlist.is_editable))
+                # get id from newly created item (the safe way)
+                async with self._db.execute('SELECT (playlist_id) FROM playlists WHERE name=? AND owner=?;', (playlist.name,playlist.owner)) as cursor:
+                    playlist_id = await cursor.fetchone()
+                    playlist_id = playlist_id[0]
+                LOGGER.debug('added playlist %s to database: %s' %(playlist.name, playlist_id))
             # add/update metadata
-            await self.__add_prov_ids(playlist_id, MediaType.Playlist, playlist.provider_ids, db)
-            await self.__add_metadata(playlist_id, MediaType.Playlist, playlist.metadata, db)
+            await self.__add_prov_ids(playlist_id, MediaType.Playlist, playlist.provider_ids)
+            await self.__add_metadata(playlist_id, MediaType.Playlist, playlist.metadata)
             # save
-            await db.commit()
+            await self._db.commit()
         return playlist_id
 
     async def add_radio(self, radio:Radio):
         ''' add a new radio record into table'''
         assert(radio.name)
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            async with db.execute('SELECT (radio_id) FROM radios WHERE name=?;', (radio.name,)) as cursor:
-                result = await cursor.fetchone()
-                if result:
-                    radio_id = result[0]
-                else:
-                    # insert radio
-                    sql_query = 'INSERT OR REPLACE INTO radios (name) VALUES(?);'
-                    await db.execute(sql_query, (radio.name,))
-                    # get id from newly created item (the safe way)
-                    async with db.execute('SELECT (radio_id) FROM radios WHERE name=?;', (radio.name,)) as cursor:
-                        radio_id = await cursor.fetchone()
-                        radio_id = radio_id[0]
-                    LOGGER.debug('added radio station %s to database: %s' %(radio.name, radio_id))
+        async with self._db.execute('SELECT (radio_id) FROM radios WHERE name=?;', (radio.name,)) as cursor:
+            result = await cursor.fetchone()
+            if result:
+                radio_id = result[0]
+            else:
+                # insert radio
+                sql_query = 'INSERT OR REPLACE INTO radios (name) VALUES(?);'
+                await self._db.execute(sql_query, (radio.name,))
+                # get id from newly created item (the safe way)
+                async with self._db.execute('SELECT (radio_id) FROM radios WHERE name=?;', (radio.name,)) as cursor:
+                    radio_id = await cursor.fetchone()
+                    radio_id = radio_id[0]
+                LOGGER.debug('added radio station %s to database: %s' %(radio.name, radio_id))
             # add/update metadata
-            await self.__add_prov_ids(radio_id, MediaType.Radio, radio.provider_ids, db)
-            await self.__add_metadata(radio_id, MediaType.Radio, radio.metadata, db)
+            await self.__add_prov_ids(radio_id, MediaType.Radio, radio.provider_ids)
+            await self.__add_metadata(radio_id, MediaType.Radio, radio.metadata)
             # save
-            await db.commit()
+            await self._db.commit()
         return radio_id
 
     async def add_to_library(self, item_id:int, media_type:MediaType, provider:str):
         ''' add an item to the library (item must already be present in the db!) '''
         item_id = try_parse_int(item_id)
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            sql_query = 'INSERT or REPLACE INTO library_items (item_id, provider, media_type) VALUES(?,?,?);'
-            await db.execute(sql_query, (item_id, provider, media_type))
-            await db.commit()
+        sql_query = 'INSERT or REPLACE INTO library_items (item_id, provider, media_type) VALUES(?,?,?);'
+        await self._db.execute(sql_query, (item_id, provider, media_type))
+        await self._db.commit()
 
     async def remove_from_library(self, item_id:int, media_type:MediaType, provider:str):
         ''' remove item from the library '''
         item_id = try_parse_int(item_id)
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            sql_query = 'DELETE FROM library_items WHERE item_id=? AND provider=? AND media_type=?;'
-            await db.execute(sql_query, (item_id, provider, media_type))
-            if media_type == MediaType.Playlist:
-                sql_query = 'DELETE FROM playlist_tracks WHERE playlist_id=?;'
-                await db.execute(sql_query, (item_id,))
-                sql_query = 'DELETE FROM playlists WHERE playlist_id=?;'
-                await db.execute(sql_query, (item_id,))
-                sql_query = 'DELETE FROM provider_mappings WHERE item_id=? AND media_type=? AND provider=?;'
-                await db.execute(sql_query, (item_id,media_type, provider))
-            await db.commit()
+        sql_query = 'DELETE FROM library_items WHERE item_id=? AND provider=? AND media_type=?;'
+        await self._db.execute(sql_query, (item_id, provider, media_type))
+        if media_type == MediaType.Playlist:
+            sql_query = 'DELETE FROM playlist_tracks WHERE playlist_id=?;'
+            await self._db.execute(sql_query, (item_id,))
+            sql_query = 'DELETE FROM playlists WHERE playlist_id=?;'
+            await self._db.execute(sql_query, (item_id,))
+            sql_query = 'DELETE FROM provider_mappings WHERE item_id=? AND media_type=? AND provider=?;'
+            await self._db.execute(sql_query, (item_id,media_type, provider))
+            await self._db.commit()
     
-    async def artists(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True, db=None) -> List[Artist]:
+    async def artists(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True) -> List[Artist]:
         ''' fetch artist records from table'''
         artists = []
         sql_query = 'SELECT * FROM artists'
@@ -258,28 +258,21 @@ class Database():
         sql_query += ' ORDER BY %s' % orderby
         if limit:
             sql_query += ' LIMIT %d OFFSET %d' %(limit, offset)
-        if not db:
-            db = await aiosqlite.connect(self.dbfile)
-            should_close_db = True
-        else:
-            should_close_db = False
-        async with db.execute(sql_query) as cursor:
+        async with self._db.execute(sql_query) as cursor:
             db_rows = await cursor.fetchall()
         for db_row in db_rows:
             artist = Artist()
             artist.item_id = db_row[0]
             artist.name = db_row[1]
             artist.sort_name = db_row[2]
-            artist.provider_ids = await self.__get_prov_ids(artist.item_id, MediaType.Artist, db)
-            artist.in_library = await self.__get_library_providers(artist.item_id, MediaType.Artist, db)
+            artist.provider_ids = await self.__get_prov_ids(artist.item_id, MediaType.Artist)
+            artist.in_library = await self.__get_library_providers(artist.item_id, MediaType.Artist)
             if fulldata:
-                artist.external_ids = await self.__get_external_ids(artist.item_id, MediaType.Artist, db)
-                artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist, db)
-                artist.tags = await self.__get_tags(artist.item_id, MediaType.Artist, db)
-                artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist, db)
+                artist.external_ids = await self.__get_external_ids(artist.item_id, MediaType.Artist)
+                artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist)
+                artist.tags = await self.__get_tags(artist.item_id, MediaType.Artist)
+                artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist)
             artists.append(artist)
-        if should_close_db:
-            await db.close()
         return artists
 
     async def artist(self, artist_id:int, fulldata=True) -> Artist:
@@ -293,39 +286,38 @@ class Database():
     async def add_artist(self, artist:Artist):
         ''' add a new artist record into table'''
         artist_id = None
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            # always prefer to grab existing artist with external_id (=musicbrainz_id)
-            artist_id = await self.__get_item_by_external_id(artist, db)
+        # always prefer to grab existing artist with external_id (=musicbrainz_id)
+        artist_id = await self.__get_item_by_external_id(artist)
+        if not artist_id:
+            # insert artist
+            musicbrainz_id = None
+            for item in artist.external_ids:
+                if item.get('musicbrainz'):
+                    musicbrainz_id = item['musicbrainz']
+                    break
+            assert(musicbrainz_id) # musicbrainz id is required
+            if not artist.sort_name:
+                artist.sort_name = get_sort_name(artist.name)
+            sql_query = 'INSERT OR IGNORE INTO artists (name, sort_name, musicbrainz_id) VALUES(?,?,?);'
+            await self._db.execute(sql_query, (artist.name, artist.sort_name, musicbrainz_id))
+            await self._db.commit()
+            # get id from (newly created) item (the safe way)
+            artist_id = await self.__get_item_by_external_id(artist)
             if not artist_id:
-                # insert artist
-                musicbrainz_id = None
-                for item in artist.external_ids:
-                    if item.get('musicbrainz'):
-                        musicbrainz_id = item['musicbrainz']
-                        break
-                assert(musicbrainz_id) # musicbrainz id is required
-                if not artist.sort_name:
-                    artist.sort_name = get_sort_name(artist.name)
-                sql_query = 'INSERT OR IGNORE INTO artists (name, sort_name, musicbrainz_id) VALUES(?,?,?);'
-                await db.execute(sql_query, (artist.name, artist.sort_name, musicbrainz_id))
-                await db.commit()
-                # get id from (newly created) item (the safe way)
-                artist_id = await self.__get_item_by_external_id(artist, db)
-                if not artist_id:
-                    async with db.execute('SELECT (artist_id) FROM artists WHERE musicbrainz_id=?;', (musicbrainz_id,)) as cursor:
-                        artist_id = await cursor.fetchone()
-                        artist_id = artist_id[0]
+                async with self._db.execute('SELECT (artist_id) FROM artists WHERE musicbrainz_id=?;', (musicbrainz_id,)) as cursor:
+                    artist_id = await cursor.fetchone()
+                    artist_id = artist_id[0]
             # add metadata and tags etc.
-            await self.__add_prov_ids(artist_id, MediaType.Artist, artist.provider_ids, db)
-            await self.__add_metadata(artist_id, MediaType.Artist, artist.metadata, db)
-            await self.__add_tags(artist_id, MediaType.Artist, artist.tags, db)
-            await self.__add_external_ids(artist_id, MediaType.Artist, artist.external_ids, db)
+            await self.__add_prov_ids(artist_id, MediaType.Artist, artist.provider_ids)
+            await self.__add_metadata(artist_id, MediaType.Artist, artist.metadata)
+            await self.__add_tags(artist_id, MediaType.Artist, artist.tags)
+            await self.__add_external_ids(artist_id, MediaType.Artist, artist.external_ids)
             # save
-            await db.commit()
+            await self._db.commit()
         LOGGER.debug('added artist %s (%s) to database: %s' %(artist.name, artist.provider_ids, artist_id))
         return artist_id
     
-    async def albums(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True, db=None) -> List[Album]:
+    async def albums(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True) -> List[Album]:
         ''' fetch all album records from table'''
         albums = []
         sql_query = 'SELECT * FROM albums'
@@ -334,12 +326,7 @@ class Database():
         sql_query += ' ORDER BY %s' % orderby
         if limit:
             sql_query += ' LIMIT %d OFFSET %d' %(limit, offset)
-        if not db:
-            db = await aiosqlite.connect(self.dbfile)
-            should_close_db = True
-        else:
-            should_close_db = False
-        async with db.execute(sql_query) as cursor:
+        async with self._db.execute(sql_query) as cursor:
             db_rows = await cursor.fetchall()
             for db_row in db_rows:
                 album = Album()
@@ -348,23 +335,21 @@ class Database():
                 album.albumtype = db_row[3]
                 album.year = db_row[4]
                 album.version = db_row[5]
-                album.provider_ids = await self.__get_prov_ids(album.item_id, MediaType.Album, db)
-                album.in_library = await self.__get_library_providers(album.item_id, MediaType.Album, db)
+                album.provider_ids = await self.__get_prov_ids(album.item_id, MediaType.Album)
+                album.in_library = await self.__get_library_providers(album.item_id, MediaType.Album)
                 if fulldata:
                     album.artist = await self.artist(db_row[1], fulldata=False)
-                    album.external_ids = await self.__get_external_ids(album.item_id, MediaType.Album, db)
-                    album.metadata = await self.__get_metadata(album.item_id, MediaType.Album, db)
-                    album.tags = await self.__get_tags(album.item_id, MediaType.Album, db)
-                    album.labels = await self.__get_album_labels(album.item_id, db)
+                    album.external_ids = await self.__get_external_ids(album.item_id, MediaType.Album)
+                    album.metadata = await self.__get_metadata(album.item_id, MediaType.Album)
+                    album.tags = await self.__get_tags(album.item_id, MediaType.Album)
+                    album.labels = await self.__get_album_labels(album.item_id)
                 albums.append(album)
-        if should_close_db:
-            await db.close()
         return albums
 
-    async def album(self, album_id:int, fulldata=True, db=None) -> Album:
+    async def album(self, album_id:int, fulldata=True) -> Album:
         ''' get album record by id '''
         album_id = try_parse_int(album_id)
-        albums = await self.albums('WHERE album_id = %s' % album_id, fulldata=fulldata, db=db)
+        albums = await self.albums('WHERE album_id = %s' % album_id, fulldata=fulldata)
         if not albums:
             return None
         return albums[0]
@@ -372,38 +357,37 @@ class Database():
     async def add_album(self, album:Album):
         ''' add a new album record into table'''
         album_id = None
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            # always try to grab existing album with external_id
-            album_id = await self.__get_item_by_external_id(album, db)
-            # fallback to matching on artist_id, name and version
-            if not album_id:
-                async with db.execute('SELECT (album_id) FROM albums WHERE artist_id=? AND name=? AND version=?;', (album.artist.item_id, album.name, album.version)) as cursor:
-                    result = await cursor.fetchone()
-                    if result:
-                        album_id = result[0]
-            if not album_id and album.year:
-                async with db.execute('SELECT (album_id) FROM albums WHERE year=? AND name=? AND version=?;', (album.year, album.name, album.version)) as cursor:
-                    result = await cursor.fetchone()
-                    if result:
-                        album_id = result[0]
-            if not album_id:
-                # insert album
-                sql_query = 'INSERT OR IGNORE INTO albums (artist_id, name, albumtype, year, version) VALUES(?,?,?,?,?);'
-                await db.execute(sql_query, (album.artist.item_id, album.name, album.albumtype, album.year, album.version))
-                await db.commit()
-                # get id from newly created item
-                async with db.execute('SELECT (album_id) FROM albums WHERE artist_id=? AND name=? AND version=?;', (album.artist.item_id, album.name, album.version)) as cursor:
-                    album_id = await cursor.fetchone()
-                    assert(album_id)
-                    album_id = album_id[0]
+        # always try to grab existing album with external_id
+        album_id = await self.__get_item_by_external_id(album)
+        # fallback to matching on artist_id, name and version
+        if not album_id:
+            async with self._db.execute('SELECT (album_id) FROM albums WHERE artist_id=? AND name=? AND version=?;', (album.artist.item_id, album.name, album.version)) as cursor:
+                result = await cursor.fetchone()
+                if result:
+                    album_id = result[0]
+        if not album_id and album.year:
+            async with self._db.execute('SELECT (album_id) FROM albums WHERE year=? AND name=? AND version=?;', (album.year, album.name, album.version)) as cursor:
+                result = await cursor.fetchone()
+                if result:
+                    album_id = result[0]
+        if not album_id:
+            # insert album
+            sql_query = 'INSERT OR IGNORE INTO albums (artist_id, name, albumtype, year, version) VALUES(?,?,?,?,?);'
+            await self._db.execute(sql_query, (album.artist.item_id, album.name, album.albumtype, album.year, album.version))
+            await self._db.commit()
+            # get id from newly created item
+            async with self._db.execute('SELECT (album_id) FROM albums WHERE artist_id=? AND name=? AND version=?;', (album.artist.item_id, album.name, album.version)) as cursor:
+                album_id = await cursor.fetchone()
+                assert(album_id)
+                album_id = album_id[0]
             # add metadata, artists and tags etc.
-            await self.__add_prov_ids(album_id, MediaType.Album, album.provider_ids, db)
-            await self.__add_metadata(album_id, MediaType.Album, album.metadata, db)
-            await self.__add_tags(album_id, MediaType.Album, album.tags, db)
-            await self.__add_album_labels(album_id, album.labels, db)
-            await self.__add_external_ids(album_id, MediaType.Album, album.external_ids, db)
+            await self.__add_prov_ids(album_id, MediaType.Album, album.provider_ids)
+            await self.__add_metadata(album_id, MediaType.Album, album.metadata)
+            await self.__add_tags(album_id, MediaType.Album, album.tags)
+            await self.__add_album_labels(album_id, album.labels)
+            await self.__add_external_ids(album_id, MediaType.Album, album.external_ids)
             # save
-            await db.commit()
+            await self._db.commit()
         LOGGER.debug('added album %s (%s) to database: %s' %(album.name, album.provider_ids, album_id))
         return album_id
 
@@ -416,30 +400,29 @@ class Database():
         sql_query += ' ORDER BY %s' % orderby
         if limit:
             sql_query += ' LIMIT %d OFFSET %d' %(limit, offset)
-        async with aiosqlite.connect(self.dbfile) as db:
-            db.row_factory = aiosqlite.Row
-            async with db.execute(sql_query) as cursor:
-                for db_row in await cursor.fetchall():
-                    track = Track()
-                    track.item_id = db_row["track_id"]
-                    track.name = db_row["name"]
-                    track.album = await self.album(db_row["album_id"], fulldata=False, db=db)
-                    track.artists = await self.__get_track_artists(track.item_id, db, fulldata=False)
-                    track.duration = db_row["duration"]
-                    track.version = db_row["version"]
-                    track.disc_number = db_row["disc_number"]
-                    track.track_number = db_row["track_number"]
-                    try:
-                        track.position = db_row["position"]
-                    except IndexError:
-                        pass
-                    track.in_library = await self.__get_library_providers(track.item_id, MediaType.Track, db)
-                    track.external_ids = await self.__get_external_ids(track.item_id, MediaType.Track, db)
-                    track.provider_ids = await self.__get_prov_ids(track.item_id, MediaType.Track, db)
-                    if fulldata:
-                        track.metadata = await self.__get_metadata(track.item_id, MediaType.Track, db)
-                        track.tags = await self.__get_tags(track.item_id, MediaType.Track, db)
-                    tracks.append(track)
+        self._db.row_factory = aiosqlite.Row
+        async with self._db.execute(sql_query) as cursor:
+            for db_row in await cursor.fetchall():
+                track = Track()
+                track.item_id = db_row["track_id"]
+                track.name = db_row["name"]
+                track.album = await self.album(db_row["album_id"], fulldata=False)
+                track.artists = await self.__get_track_artists(track.item_id, fulldata=False)
+                track.duration = db_row["duration"]
+                track.version = db_row["version"]
+                track.disc_number = db_row["disc_number"]
+                track.track_number = db_row["track_number"]
+                try:
+                    track.position = db_row["position"]
+                except IndexError:
+                    pass
+                track.in_library = await self.__get_library_providers(track.item_id, MediaType.Track)
+                track.external_ids = await self.__get_external_ids(track.item_id, MediaType.Track)
+                track.provider_ids = await self.__get_prov_ids(track.item_id, MediaType.Track)
+                if fulldata:
+                    track.metadata = await self.__get_metadata(track.item_id, MediaType.Track)
+                    track.tags = await self.__get_tags(track.item_id, MediaType.Track)
+                tracks.append(track)
         return tracks
 
     async def track(self, track_id:int, fulldata=True) -> Track:
@@ -455,51 +438,49 @@ class Database():
         ''' add a new track record into table'''
         assert(track.name and track.album)
         track_id = None
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            # always try to grab existing track with external_id
-            track_id = await self.__get_item_by_external_id(track, db)
-            # fallback to matching on album_id, name and version or track number
-            if not track_id and track.track_number:
-                async with db.execute('SELECT (track_id) FROM tracks WHERE album_id=? AND track_number=?;', (track.album.item_id, track.track_number)) as cursor:
-                    result = await cursor.fetchone()
-                    if result:
-                        track_id = result[0]
-            if not track_id:
-                async with db.execute('SELECT (track_id) FROM tracks WHERE album_id=? AND name=? AND version=?;', (track.album.item_id, track.name, track.version)) as cursor:
-                    result = await cursor.fetchone()
-                    if result:
-                        track_id = result[0]
-            if not track_id:
-                # insert track
-                assert(track.name and track.album.item_id)
-                sql_query = 'INSERT OR IGNORE INTO tracks (name, album_id, duration, version, disc_number, track_number) VALUES(?,?,?,?,?,?);'
-                await db.execute(sql_query, (track.name, track.album.item_id, track.duration, track.version, track.disc_number, track.track_number))
-                await db.commit()
-                # get id from newly created item (the safe way)
-                async with db.execute('SELECT (track_id) FROM tracks WHERE name=? AND album_id=? AND version=?;', (track.name, track.album.item_id, track.version)) as cursor:
-                    track_id = await cursor.fetchone()
-                    assert(track_id)
-                    track_id = track_id[0]
-            # add track artists
-            for artist in track.artists:
-                sql_query = 'INSERT or IGNORE INTO track_artists (track_id, artist_id) VALUES(?,?);'
-                await db.execute(sql_query, (track_id, artist.item_id))
+        # always try to grab existing track with external_id
+        track_id = await self.__get_item_by_external_id(track)
+        # fallback to matching on album_id, name and version or track number
+        if not track_id and track.track_number:
+            async with self._db.execute('SELECT (track_id) FROM tracks WHERE album_id=? AND track_number=?;', (track.album.item_id, track.track_number)) as cursor:
+                result = await cursor.fetchone()
+                if result:
+                    track_id = result[0]
+        if not track_id:
+            async with self._db.execute('SELECT (track_id) FROM tracks WHERE album_id=? AND name=? AND version=?;', (track.album.item_id, track.name, track.version)) as cursor:
+                result = await cursor.fetchone()
+                if result:
+                    track_id = result[0]
+        if not track_id:
+            # insert track
+            assert(track.name and track.album.item_id)
+            sql_query = 'INSERT OR IGNORE INTO tracks (name, album_id, duration, version, disc_number, track_number) VALUES(?,?,?,?,?,?);'
+            await self._db.execute(sql_query, (track.name, track.album.item_id, track.duration, track.version, track.disc_number, track.track_number))
+            await self._db.commit()
+            # get id from newly created item (the safe way)
+            async with self._db.execute('SELECT (track_id) FROM tracks WHERE name=? AND album_id=? AND version=?;', (track.name, track.album.item_id, track.version)) as cursor:
+                track_id = await cursor.fetchone()
+                assert(track_id)
+                track_id = track_id[0]
+        # add track artists
+        for artist in track.artists:
+            sql_query = 'INSERT or IGNORE INTO track_artists (track_id, artist_id) VALUES(?,?);'
+            await self._db.execute(sql_query, (track_id, artist.item_id))
             # add metadata, tags and artists etc.
-            await self.__add_prov_ids(track_id, MediaType.Track, track.provider_ids, db)
-            await self.__add_metadata(track_id, MediaType.Track, track.metadata, db)
-            await self.__add_tags(track_id, MediaType.Track, track.tags, db)
-            await self.__add_external_ids(track_id, MediaType.Track, track.external_ids, db)
+            await self.__add_prov_ids(track_id, MediaType.Track, track.provider_ids)
+            await self.__add_metadata(track_id, MediaType.Track, track.metadata)
+            await self.__add_tags(track_id, MediaType.Track, track.tags)
+            await self.__add_external_ids(track_id, MediaType.Track, track.external_ids)
             # save to db
-            await db.commit()
+            await self._db.commit()
         LOGGER.debug('added track %s (%s) to database: %s' %(track.name, track.provider_ids, track_id))
         return track_id
 
     async def update_track(self, track_id, column_key, column_value):
         ''' update column of existing track '''
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            sql_query = 'UPDATE tracks SET %s=%s WHERE track_id=%s;' %(column_key, column_value, track_id)
-            await db.execute(sql_query)
-            await db.commit()
+        sql_query = 'UPDATE tracks SET %s=%s WHERE track_id=%s;' %(column_key, column_value, track_id)
+        await self._db.execute(sql_query)
+        await self._db.commit()
 
     async def artist_tracks(self, artist_id, limit=1000000, offset=0, orderby='name') -> List[Track]:
         ''' get all library tracks for the given artist '''
@@ -527,50 +508,46 @@ class Database():
 
     async def add_playlist_track(self, playlist_id:int, track_id, position):
         ''' add playlist track to playlist '''
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            sql_query = 'INSERT or REPLACE INTO playlist_tracks (playlist_id, track_id, position) VALUES(?,?,?);'
-            await db.execute(sql_query, (playlist_id, track_id, position))
-            await db.commit()
+        sql_query = 'INSERT or REPLACE INTO playlist_tracks (playlist_id, track_id, position) VALUES(?,?,?);'
+        await self._db.execute(sql_query, (playlist_id, track_id, position))
+        await self._db.commit()
 
     async def remove_playlist_track(self, playlist_id:int, track_id):
         ''' remove playlist track from playlist '''
-        async with aiosqlite.connect(self.dbfile, timeout=20) as db:
-            sql_query = 'DELETE FROM playlist_tracks WHERE playlist_id=? AND track_id=?;'
-            await db.execute(sql_query, (playlist_id, track_id))
-            await db.commit()
+        sql_query = 'DELETE FROM playlist_tracks WHERE playlist_id=? AND track_id=?;'
+        await self._db.execute(sql_query, (playlist_id, track_id))
+        await self._db.commit()
             
     async def set_track_loudness(self, provider_track_id, provider, loudness):
         ''' set integrated loudness for a track in db '''
-        async with aiosqlite.connect(self.dbfile) as db:
-            sql_query = 'INSERT or REPLACE INTO track_loudness (provider_track_id, provider, loudness) VALUES(?,?,?);'
-            await db.execute(sql_query, (provider_track_id, provider, loudness))
-            await db.commit()
+        sql_query = 'INSERT or REPLACE INTO track_loudness (provider_track_id, provider, loudness) VALUES(?,?,?);'
+        await self._db.execute(sql_query, (provider_track_id, provider, loudness))
+        await self._db.commit()
 
     async def get_track_loudness(self, provider_track_id, provider):
         ''' get integrated loudness for a track in db '''
-        async with aiosqlite.connect(self.dbfile) as db:
-            sql_query = 'SELECT loudness FROM track_loudness WHERE provider_track_id = ? AND provider = ?'
-            async with db.execute(sql_query, (provider_track_id, provider)) as cursor:
-                result = await cursor.fetchone()
-            if result:
-                return result[0]
-            else:
-                return None
+        sql_query = 'SELECT loudness FROM track_loudness WHERE provider_track_id = ? AND provider = ?'
+        async with self._db.execute(sql_query, (provider_track_id, provider)) as cursor:
+            result = await cursor.fetchone()
+        if result:
+            return result[0]
+        else:
+            return None
 
-    async def __add_metadata(self, item_id, media_type, metadata, db):
+    async def __add_metadata(self, item_id, media_type, metadata):
         ''' add or update metadata'''
         for key, value in metadata.items():
             if value:
                 sql_query = 'INSERT or REPLACE INTO metadata (item_id, media_type, key, value) VALUES(?,?,?,?);'
-                await db.execute(sql_query, (item_id, media_type, key, value))
+                await self._db.execute(sql_query, (item_id, media_type, key, value))
 
-    async def __get_metadata(self, item_id, media_type, db, filter_key=None):
+    async def __get_metadata(self, item_id, media_type, filter_key=None):
         ''' get metadata for media item '''
         metadata = {}
         sql_query = 'SELECT key, value FROM metadata WHERE item_id = ? AND media_type = ?'
         if filter_key:
             sql_query += ' AND key = "%s"' % filter_key
-        async with db.execute(sql_query, (item_id, media_type)) as cursor:
+        async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
             db_rows = await cursor.fetchall()
         for db_row in db_rows:
             key = db_row[0]
@@ -578,61 +555,61 @@ class Database():
             metadata[key] = value
         return metadata
 
-    async def __add_tags(self, item_id, media_type, tags, db):
+    async def __add_tags(self, item_id, media_type, tags):
         ''' add tags to db '''
         for tag in tags:
             sql_query = 'INSERT or IGNORE INTO tags (name) VALUES(?);'
-            async with db.execute(sql_query, (tag,)) as cursor:
+            async with self._db.execute(sql_query, (tag,)) as cursor:
                 tag_id = cursor.lastrowid
             sql_query = 'INSERT or IGNORE INTO media_tags (item_id, media_type, tag_id) VALUES(?,?,?);'
-            await db.execute(sql_query, (item_id, media_type, tag_id))
+            await self._db.execute(sql_query, (item_id, media_type, tag_id))
 
-    async def __get_tags(self, item_id, media_type, db):
+    async def __get_tags(self, item_id, media_type):
         ''' get tags for media item '''
         tags = []
         sql_query = 'SELECT name FROM tags INNER JOIN media_tags on tags.tag_id = media_tags.tag_id WHERE item_id = ? AND media_type = ?'
-        async with db.execute(sql_query, (item_id, media_type)) as cursor:
+        async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
             db_rows = await cursor.fetchall()
         for db_row in db_rows:
             tags.append(db_row[0])
         return tags
     
-    async def __add_album_labels(self, album_id, labels, db):
+    async def __add_album_labels(self, album_id, labels):
         ''' add labels to album in db '''
         for label in labels:
             sql_query = 'INSERT or IGNORE INTO labels (name) VALUES(?);'
-            async with db.execute(sql_query, (label,)) as cursor:
+            async with self._db.execute(sql_query, (label,)) as cursor:
                 label_id = cursor.lastrowid
             sql_query = 'INSERT or IGNORE INTO album_labels (album_id, label_id) VALUES(?,?);'
-            await db.execute(sql_query, (album_id, label_id))
+            await self._db.execute(sql_query, (album_id, label_id))
 
-    async def __get_album_labels(self, album_id, db):
+    async def __get_album_labels(self, album_id):
         ''' get labels for album item '''
         labels = []
         sql_query = 'SELECT name FROM labels INNER JOIN album_labels on labels.label_id = album_labels.label_id WHERE album_id = ?'
-        async with db.execute(sql_query, (album_id,)) as cursor:
+        async with self._db.execute(sql_query, (album_id,)) as cursor:
             db_rows = await cursor.fetchall()
         for db_row in db_rows:
             labels.append(db_row[0])
         return labels
     
-    async def __get_track_artists(self, track_id, db, fulldata=False) -> List[Artist]:
+    async def __get_track_artists(self, track_id, fulldata=False) -> List[Artist]:
         ''' get artists for track '''
         sql_query = 'WHERE artist_id in (SELECT artist_id FROM track_artists WHERE track_id = %s)' % track_id
-        return await self.artists(sql_query, db=db, fulldata=fulldata)
+        return await self.artists(sql_query, fulldata=fulldata)
     
-    async def __add_external_ids(self, item_id, media_type, external_ids, db):
+    async def __add_external_ids(self, item_id, media_type, external_ids):
         ''' add or update external_ids'''
         for external_id in external_ids:
             for key, value in external_id.items():
                 sql_query = 'INSERT or REPLACE INTO external_ids (item_id, media_type, key, value) VALUES(?,?,?,?);'
-                await db.execute(sql_query, (item_id, media_type, key, value))
+                await self._db.execute(sql_query, (item_id, media_type, key, value))
 
-    async def __get_external_ids(self, item_id, media_type, db):
+    async def __get_external_ids(self, item_id, media_type):
         ''' get external_ids for media item '''
         external_ids = []
         sql_query = 'SELECT key, value FROM external_ids WHERE item_id = ? AND media_type = ?'
-        async with db.execute(sql_query, (item_id, media_type)) as cursor:
+        async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
             db_rows = await cursor.fetchall()
         for db_row in db_rows:
             external_id = {
@@ -641,7 +618,7 @@ class Database():
             external_ids.append(external_id)
         return external_ids
 
-    async def __add_prov_ids(self, item_id, media_type, provider_ids, db):
+    async def __add_prov_ids(self, item_id, media_type, provider_ids):
         ''' add provider ids for media item to db '''
         for prov_mapping in provider_ids:
             prov_id = prov_mapping['provider']
@@ -649,15 +626,15 @@ class Database():
             quality = prov_mapping.get('quality',0)
             details = prov_mapping.get('details','')
             sql_query = 'INSERT OR REPLACE INTO provider_mappings (item_id, media_type, prov_item_id, provider, quality, details) VALUES(?,?,?,?,?,?);'
-            await db.execute(sql_query, (item_id, media_type, prov_item_id, prov_id, quality, details))
+            await self._db.execute(sql_query, (item_id, media_type, prov_item_id, prov_id, quality, details))
 
-    async def __get_prov_ids(self, item_id, media_type:MediaType, db):
+    async def __get_prov_ids(self, item_id, media_type:MediaType):
         ''' get all provider_ids for media item '''
         provider_ids = []
         sql_query = 'SELECT prov_item_id, provider, quality, details \
             FROM provider_mappings \
             WHERE item_id = ? AND media_type = ?'
-        async with db.execute(sql_query, (item_id, media_type)) as cursor:
+        async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
             db_rows = await cursor.fetchall()
         for db_row in db_rows:
             prov_mapping = {
@@ -669,24 +646,24 @@ class Database():
             provider_ids.append(prov_mapping)
         return provider_ids
 
-    async def __get_library_providers(self, item_id, media_type:MediaType, db):
+    async def __get_library_providers(self, item_id, media_type:MediaType):
         ''' get the providers that have this media_item added to the library '''
         providers = []
         sql_query = 'SELECT provider FROM library_items WHERE item_id = ? AND media_type = ?'
-        async with db.execute(sql_query, (item_id, media_type)) as cursor:
+        async with self._db.execute(sql_query, (item_id, media_type)) as cursor:
             db_rows = await cursor.fetchall()
         for db_row in db_rows:
             providers.append( db_row[0] )
         return providers
 
-    async def __get_item_by_external_id(self, media_item, db):
+    async def __get_item_by_external_id(self, media_item):
         ''' try to get existing item in db by matching the new item's external id's '''
         item_id = None
         for external_id in media_item.external_ids:
             if item_id:
                 break
             for key, value in external_id.items():
-                async with db.execute('SELECT (item_id) FROM external_ids WHERE media_type=? AND key=? AND value=?;', (media_item.media_type, key, value)) as cursor:
+                async with self._db.execute('SELECT (item_id) FROM external_ids WHERE media_type=? AND key=? AND value=?;', (media_item.media_type, key, value)) as cursor:
                     result = await cursor.fetchone()
                     if result:
                         item_id = result[0]
index 3e54ea7a08e445c187f8e32ed87764d0207dee52..e6cf2fac25d8a51b8fc5ea271eabfaaa43ce04a0 100644 (file)
@@ -34,9 +34,9 @@ class ChromecastPlayer(Player):
     ''' Chromecast player object '''
     
     def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
         self.__cc_report_progress_task = None
-
+        super().__init__(*args, **kwargs)
+        
     def __del__(self):
         if self.__cc_report_progress_task:
             self.__cc_report_progress_task.cancel()