浏览代码

Merge branch 'dev' of github.com:allanpk716/ChineseSubFinder into dev

allan716 3 年之前
父节点
当前提交
f2b3da4d25

+ 12 - 0
frontend/src/api/LibraryApi.js

@@ -0,0 +1,12 @@
+import BaseApi from './BaseApi';
+
+class LibraryApi extends BaseApi {
+  getRefreshStatus = () => this.http('/v1/video/list/refresh-status');
+
+  refreshLibrary = () => this.http('/v1/video/list/refresh', {}, 'POST');
+
+  getList = () => this.http('/v1/video/list');
+
+  downloadSubtitle = (videoId) => this.http(`/v1/video/subtitle/download`, { id: videoId }, 'POST');
+}
+export default new LibraryApi();

+ 1 - 0
frontend/src/layouts/MenuItem.vue

@@ -4,6 +4,7 @@
       v-if="menuInfo.children && menuInfo.children.length"
       expand-separator
       :label="menuInfo.meta.title"
+      :icon="menuInfo.meta.icon"
       :default-opened="defaultOpened"
     >
       <menu-item class="q-pl-md" v-for="subMenu in menuInfo.children" :menu-info="subMenu" :key="subMenu.name" />

+ 0 - 62
frontend/src/pages/library/index.vue

@@ -1,62 +0,0 @@
-<template>
-  <q-page class="q-pa-lg">
-    <div class="row q-gutter-md">
-      <q-select
-        v-model="filterForm.hasSubtitle"
-        dense
-        outlined
-        :options="[
-          { label: '有字幕', value: true },
-          { label: '无字幕', value: false },
-        ]"
-        label="有无字幕"
-        clearable
-        style="width: 200px"
-      />
-      <q-checkbox label="已跳过" :val="true" v-model="filterForm.skipped" />
-    </div>
-    <q-separator class="q-my-md" />
-    <div class="row q-gutter-x-md q-gutter-y-lg">
-      <q-card v-for="i in 100" :key="i" flat square>
-        <div class="q-mb-sm">
-          <q-img src="https://via.placeholder.com/500" class="content-width bg-grey-2" height="230px" no-spinner />
-        </div>
-        <div class="content-width text-ellipsis-line-2">
-          Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet
-        </div>
-        <div class="row items-center">
-          <div class="text-grey">2021-10-31</div>
-          <q-space />
-          <div>
-            <q-btn color="black" round flat dense icon="closed_caption" @click.stop title="已有字幕" />
-            <!-- <q-btn color="grey" round flat dense icon="closed_caption" @click.stop title="没有字幕" />-->
-            <!-- <q-btn color="black" round flat dense icon="closed_caption_disabled" @click.stop title="已跳过" />-->
-          </div>
-        </div>
-      </q-card>
-    </div>
-  </q-page>
-</template>
-
-<script setup>
-import { reactive } from 'vue';
-
-const filterForm = reactive({
-  skipped: false,
-  hasSubtitle: undefined,
-});
-</script>
-
-<style scoped>
-.content-width {
-  width: 160px;
-}
-.text-ellipsis-line-2 {
-  height: 40px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-}
-</style>

+ 86 - 0
frontend/src/pages/library/movies/ListItemMovie.vue

@@ -0,0 +1,86 @@
+<template>
+  <q-card flat square>
+    <div class="area-cover q-mb-sm relative-position">
+      <q-img src="https://via.placeholder.com/500" class="content-width bg-grey-2" height="230px" no-spinner />
+      <q-btn
+        class="btn-download absolute-bottom-right"
+        color="primary"
+        round
+        flat
+        dense
+        icon="download_for_offline"
+        title="下载字幕"
+        @click="downloadSubtitle"
+      ></q-btn>
+    </div>
+    <div class="content-width text-ellipsis-line-2" :title="data.name">{{ data.name }}</div>
+    <div class="row items-center">
+      <div class="text-grey">1970-01-01</div>
+      <q-space />
+      <div>
+        <q-btn v-if="hasSubtitle" color="black" round flat dense icon="closed_caption" @click.stop title="已有字幕">
+          <q-popup-proxy>
+            <q-list dense>
+              <q-item v-for="(item, index) in data.sub_f_path_list" :key="item">
+                <q-item-section side>{{ index + 1 }}.</q-item-section>
+
+                <q-item-section class="overflow-hidden ellipsis" :title="item.split(/\/|\\/).pop()">
+                  <a class="text-primary" href="" target="_blank">{{ item.split(/\/|\\/).pop() }}</a>
+                </q-item-section>
+              </q-item>
+            </q-list>
+          </q-popup-proxy>
+        </q-btn>
+        <q-btn v-else color="grey" round flat dense icon="closed_caption" @click.stop title="没有字幕" />
+      </div>
+    </div>
+  </q-card>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import LibraryApi from 'src/api/LibraryApi';
+import { SystemMessage } from 'src/utils/Message';
+
+const props = defineProps({
+  data: Object,
+});
+
+const hasSubtitle = computed(() => props.data.sub_f_path_list.length > 0);
+
+const downloadSubtitle = async () => {
+  const [, err] = await LibraryApi.downloadSubtitle(props.data.id);
+  if (err !== null) {
+    SystemMessage.error(err.message);
+  } else {
+    SystemMessage.success('已加入下载队列');
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.content-width {
+  width: 160px;
+}
+.text-ellipsis-line-2 {
+  height: 40px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.btn-download {
+  //display: none;
+  opacity: 0;
+  transition: all 0.6s ease;
+}
+
+.area-cover:hover {
+  .btn-download {
+    //display: block;
+    opacity: 1;
+  }
+}
+</style>

+ 58 - 0
frontend/src/pages/library/movies/index.vue

@@ -0,0 +1,58 @@
+<template>
+  <q-page class="q-pa-lg">
+    <div class="row q-gutter-md">
+      <q-btn label="更新缓存" color="primary" icon="cached" @click="refreshLibrary" :loading="refreshCacheLoading">
+        <template v-slot:loading>
+          <q-spinner-hourglass class="on-left" />
+          更新缓存中...
+        </template>
+      </q-btn>
+
+      <q-space />
+
+      <q-select
+        v-model="filterForm.hasSubtitle"
+        dense
+        outlined
+        :options="[
+          { label: '有字幕', value: true },
+          { label: '无字幕', value: false },
+        ]"
+        label="有无字幕"
+        clearable
+        emit-value
+        map-options
+        style="width: 200px"
+      />
+    </div>
+
+    <q-separator class="q-my-md" />
+
+    <div v-if="movies.length" class="row q-gutter-x-md q-gutter-y-lg">
+      <list-item-movie v-for="item in filteredMovies" :data="item" :key="item.name" />
+    </div>
+    <div v-else class="q-my-md text-grey">当前没有可用视频,点击"更新缓存"按钮可重建缓存</div>
+  </q-page>
+</template>
+
+<script setup>
+import { useLibrary } from 'pages/library/useLibrary';
+import { computed, reactive } from 'vue';
+import ListItemMovie from './ListItemMovie';
+
+const filterForm = reactive({
+  hasSubtitle: null,
+});
+
+const { movies, refreshLibrary, refreshCacheLoading } = useLibrary();
+
+const filteredMovies = computed(() => {
+  if (filterForm.hasSubtitle === null) {
+    return movies.value;
+  }
+  if (filterForm.hasSubtitle === true) {
+    return movies.value.filter((item) => item.sub_f_path_list.length > 0);
+  }
+  return movies.value.filter((item) => item.sub_f_path_list.length === 0);
+});
+</script>

+ 108 - 0
frontend/src/pages/library/tvs/DialogTVDetail.vue

@@ -0,0 +1,108 @@
+<template>
+  <span @click="visible = true">
+    <slot></slot>
+  </span>
+
+  <q-dialog v-model="visible">
+    <q-card style="width: 400px">
+      <q-card-section>
+        <div class="text-h6">{{ data.name }} 剧集列表</div>
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-section>
+        <q-list dense>
+          <q-item v-for="item in sortedVideos" :key="item.name">
+            <q-item-section>第 {{ item.season }} 季 {{ pandStart2(item.episode) }} 集</q-item-section>
+            <q-item-section side>
+              <q-btn
+                v-if="item.sub_f_path_list.length"
+                color="black"
+                round
+                flat
+                dense
+                icon="closed_caption"
+                @click.stop
+                title="已有字幕"
+              >
+                <q-popup-proxy anchor="top right">
+                  <q-list dense>
+                    <q-item v-for="(item1, index) in item.sub_f_path_list" :key="item1">
+                      <q-item-section side>{{ index + 1 }}.</q-item-section>
+
+                      <q-item-section class="overflow-hidden ellipsis" :title="item1.split(/\/|\\/).pop()">
+                        <a class="text-primary" href="" target="_blank">{{ item1.split(/\/|\\/).pop() }}</a>
+                      </q-item-section>
+                    </q-item>
+                  </q-list>
+                </q-popup-proxy>
+              </q-btn>
+              <q-btn v-else color="grey" round flat dense icon="closed_caption" @click.stop title="没有字幕" />
+            </q-item-section>
+
+            <q-item-section side>
+              <q-btn
+                class="btn-download absolute-bottom-right"
+                color="primary"
+                round
+                flat
+                dense
+                icon="download_for_offline"
+                title="下载字幕"
+                @click="downloadSubtitle(item.id)"
+              ></q-btn>
+            </q-item-section>
+          </q-item>
+        </q-list>
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue';
+import LibraryApi from 'src/api/LibraryApi';
+import { SystemMessage } from 'src/utils/Message';
+
+const props = defineProps({
+  data: Object,
+});
+
+// 按季度、剧集排序
+const sortedVideos = computed(() =>
+  [...props.data.one_video_info].sort((a, b) => {
+    if (a.season > b.season) {
+      return 1;
+    }
+    if (a.season < b.season) {
+      return -1;
+    }
+    if (a.episode > b.episode) {
+      return 1;
+    }
+    if (a.episode < b.episode) {
+      return -1;
+    }
+    return 0;
+  })
+);
+
+const pandStart2 = (num) => {
+  if (num < 10) {
+    return `0${num}`;
+  }
+  return num;
+};
+
+const visible = ref(false);
+
+const downloadSubtitle = async (id) => {
+  const [, err] = await LibraryApi.downloadSubtitle(id);
+  if (err !== null) {
+    SystemMessage.error(err.message);
+  } else {
+    SystemMessage.success('已加入下载队列');
+  }
+};
+</script>

+ 66 - 0
frontend/src/pages/library/tvs/ListItemTV.vue

@@ -0,0 +1,66 @@
+<template>
+  <q-card flat square>
+    <div class="area-cover q-mb-sm relative-position">
+      <q-img src="https://via.placeholder.com/500" class="content-width bg-grey-2" height="230px" no-spinner />
+    </div>
+    <div class="content-width text-ellipsis-line-2" :title="data.name">{{ data.name }}</div>
+    <div class="row items-center">
+      <div class="text-grey">1970-01-01</div>
+      <q-space />
+      <div>
+        <dialog-t-v-detail :data="data">
+          <q-btn
+            v-if="hasSubtitleVideoCount > 0"
+            color="black"
+            flat
+            dense
+            icon="closed_caption"
+            :label="`${hasSubtitleVideoCount}/${data.one_video_info.length}`"
+            title="已有字幕"
+          />
+          <q-btn v-else color="grey" round flat dense icon="closed_caption" title="没有字幕" />
+        </dialog-t-v-detail>
+      </div>
+    </div>
+  </q-card>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import DialogTVDetail from 'pages/library/tvs/DialogTVDetail';
+
+const props = defineProps({
+  data: Object,
+});
+
+const hasSubtitleVideoCount = computed(
+  () => props.data.one_video_info.filter((e) => e.sub_f_path_list.length > 0).length
+);
+</script>
+
+<style lang="scss" scoped>
+.content-width {
+  width: 160px;
+}
+.text-ellipsis-line-2 {
+  height: 40px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.btn-download {
+  //display: none;
+  opacity: 0;
+  transition: all 0.6s ease;
+}
+
+.area-cover:hover {
+  .btn-download {
+    //display: block;
+    opacity: 1;
+  }
+}
+</style>

+ 46 - 0
frontend/src/pages/library/tvs/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <q-page class="q-pa-lg">
+    <div class="row q-gutter-md">
+      <q-btn label="更新缓存" color="primary" icon="cached" @click="refreshLibrary" :loading="refreshCacheLoading">
+        <template v-slot:loading>
+          <q-spinner-hourglass class="on-left" />
+          更新缓存中...
+        </template>
+      </q-btn>
+
+      <q-space />
+
+      <q-select
+        v-model="filterForm.hasSubtitle"
+        dense
+        outlined
+        :options="[
+          { label: '有字幕', value: true },
+          { label: '无字幕', value: false },
+        ]"
+        label="有无字幕"
+        clearable
+        style="width: 200px"
+      />
+    </div>
+
+    <q-separator class="q-my-md" />
+
+    <div v-if="tvs.length" class="row q-gutter-x-md q-gutter-y-lg">
+      <list-item-t-v v-for="item in tvs" :data="item" :key="item.name" />
+    </div>
+    <div v-else class="q-my-md text-grey">当前没有可用视频,点击"更新缓存"按钮可重建缓存</div>
+  </q-page>
+</template>
+
+<script setup>
+import { useLibrary } from 'pages/library/useLibrary';
+import { reactive } from 'vue';
+import ListItemTV from 'pages/library/tvs/ListItemTV';
+
+const filterForm = reactive({
+  hasSubtitle: undefined,
+});
+
+const { tvs, refreshLibrary, refreshCacheLoading } = useLibrary();
+</script>

+ 62 - 0
frontend/src/pages/library/useLibrary.js

@@ -0,0 +1,62 @@
+import { onBeforeUnmount, onMounted, ref } from 'vue';
+import LibraryApi from 'src/api/LibraryApi';
+import { SystemMessage } from 'src/utils/Message';
+import { until } from '@vueuse/core';
+
+export const useLibrary = () => {
+  const movies = ref([]);
+  const tvs = ref([]);
+  const refreshCacheLoading = ref(false);
+  const libraryRefreshStatus = ref(null);
+  let getRefreshStatusTimer = null;
+
+  const getLibraryRefreshStatus = async () => {
+    const [res] = await LibraryApi.getRefreshStatus();
+    libraryRefreshStatus.value = res.status;
+  };
+
+  const getLibraryList = async () => {
+    const [res, err] = await LibraryApi.getList();
+    if (err !== null) {
+      SystemMessage.error(err.message);
+    } else {
+      movies.value = res.movie_infos;
+      tvs.value = res.season_infos;
+    }
+  };
+
+  const refreshLibrary = async () => {
+    refreshCacheLoading.value = true;
+    const [, err] = await LibraryApi.refreshLibrary();
+    if (err !== null) {
+      SystemMessage.error(err.message);
+    } else {
+      libraryRefreshStatus.value = null;
+      getRefreshStatusTimer = setInterval(() => {
+        getLibraryRefreshStatus();
+      }, 1000);
+      await until(libraryRefreshStatus).toBe('stopped');
+      clearInterval(getRefreshStatusTimer);
+      getRefreshStatusTimer = null;
+      getLibraryList();
+      SystemMessage.success('更新成功');
+    }
+    refreshCacheLoading.value = false;
+  };
+
+  onMounted(() => {
+    getLibraryList();
+    getLibraryRefreshStatus();
+  });
+
+  onBeforeUnmount(() => {
+    clearInterval(getRefreshStatusTimer);
+  });
+
+  return {
+    movies,
+    tvs,
+    refreshLibrary,
+    refreshCacheLoading,
+  };
+};

+ 20 - 6
frontend/src/router/routes.js

@@ -6,18 +6,32 @@ const routes = [
     component: () => import('layouts/MainLayout.vue'),
     redirect: { name: 'overview' },
     children: [
-      // {
-      //   name: 'library',
-      //   path: 'library',
-      //   component: () => import('pages/library/index.vue'),
-      //   meta: { title: '库', icon: 'video_library' },
-      // },
       {
         name: 'overview',
         path: 'overview',
         component: () => import('pages/overview/index.vue'),
         meta: { title: '总览', icon: 'home' },
       },
+      {
+        name: 'library',
+        path: 'library',
+        component: RouterPlaceholder,
+        meta: { title: '库', icon: 'video_library' },
+        children: [
+          {
+            name: 'library.movie.list',
+            path: 'library/movies',
+            component: () => import('pages/library/movies/index.vue'),
+            meta: { title: '电影', icon: 'movie' },
+          },
+          {
+            name: 'library.tv.list',
+            path: 'library/tvs',
+            component: () => import('pages/library/tvs/index.vue'),
+            meta: { title: '连续剧', icon: 'live_tv' },
+          },
+        ],
+      },
       {
         name: 'jobs',
         path: 'jobs',