Skip to content
This repository has been archived by the owner on Mar 26, 2023. It is now read-only.

Commit

Permalink
feat: Прототип автоматического выбора предпочитаемого перевода
Browse files Browse the repository at this point in the history
  • Loading branch information
cawa-93 committed May 21, 2021
1 parent f9ddd14 commit b13e609
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 40 deletions.
100 changes: 75 additions & 25 deletions packages/renderer/src/components/WatchPage/WatchPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<script lang="ts">
import {asyncComputed} from '@vueuse/core';
import type {DeepReadonly} from 'vue';
import {computed, defineComponent, ref, watchEffect} from 'vue';
import {computed, defineComponent, ref, watch, watchEffect} from 'vue';
import type {Episode, Translation, Video} from '/@/utils/videoProvider';
import {clearVideosCache, getEpisodes, getTranslations, getVideos} from '/@/utils/videoProvider';
import SidePanel from '/@/components/WatchPage/SidePanel.vue';
Expand All @@ -69,6 +69,7 @@ import VideoPlayer from '/@/components/WatchPage/VideoPlayer/VideoPlayer.vue';
import WinIcon from '/@/components/WinIcon.vue';
import {useRouter} from 'vue-router';
import {showErrorMessage} from '/@/utils/dialogs';
import {getPreferredTranslationFromList} from '/@/utils/translationRecomendations';
Expand All @@ -91,62 +92,111 @@ export default defineComponent({
},
},
setup(props) {
const router = useRouter();
// Эпизоды
const episodes = asyncComputed(() => props.seriesId ? getEpisodes(props.seriesId) : [] as Episode[], [] as Episode[]);
const selectedEpisode = computed(() => episodes.value.find(e => e.number == props.episodeNum) || episodes.value[0]);
const router = useRouter();
const nextEpisodeURL = computed(() => {
const nextEpisodeURL = ref<string | undefined>();
const getNextEpisodeUrl = async () => {
const nextEpisode = episodes.value[episodes.value.findIndex(e => e === selectedEpisode.value) + 1];
if (nextEpisode === undefined) {
return undefined;
}
const resolved = router.resolve({params: {episodeNum: nextEpisode.number, translationId: ''}, hash: ''});
const nextEpisodeTranslations = await getTranslations(nextEpisode.id);
const nextEpisodePreferredTranslations = await getPreferredTranslationFromList(props.seriesId, nextEpisodeTranslations as Translation[]);
const translationId = nextEpisodePreferredTranslations?.id || '';
// Если удалось определить перевод для следующей серии -- выполнить загрузку видео, чтобы кэшировать их
if (nextEpisodePreferredTranslations?.id) {
console.info('Попытка предзагрузки видео');
getVideos(nextEpisodePreferredTranslations?.id).catch(e => console.error('предзагрузить видео не удалось', e));
}
const resolved = router.resolve({params: {episodeNum: nextEpisode.number, translationId}, hash: ''});
if (!resolved) {
console.error('Не удалось определить ссылку на следующую серию', {resolved});
return undefined;
}
return resolved.href;
});
};
watch(selectedEpisode, async () => nextEpisodeURL.value = await getNextEpisodeUrl());
// Доступные переводы
const translations = asyncComputed(() => selectedEpisode.value ? getTranslations(selectedEpisode.value.id) : [] as Translation[], [] as Translation[]);
const selectedTranslation = computed(() => translations.value.find(e => e.id === props.translationId) || translations.value[0]);
const translations = ref<DeepReadonly<Translation[]>>([]);
watch(selectedEpisode, async () => translations.value = await getTranslations(selectedEpisode.value.id));
const selectedTranslation = computed(() => translations.value.find(e => e.id === props.translationId));
// Автоматический выбор наиболее предпочитаемого перевода
watch(translations, () => {
if (!props.translationId && translations.value.length) {
getPreferredTranslationFromList(props.seriesId, translations.value as Translation[]).then(t => {
if (!t?.id) {
return;
}
router.replace({
params: {
episodeNum: selectedEpisode.value.number,
translationId: t.id,
},
});
});
}
});
// Загрузка доступных видео для выбранного перевода
const videos = ref<DeepReadonly<Video[]>>([]);
const loadVideoSources = () => getVideos(selectedTranslation.value.id)
.then(v => videos.value = v)
.catch((err) => {
const loadVideoSources = () => {
if (!selectedTranslation.value?.id) {
return;
}
return getVideos(selectedTranslation.value.id)
.then(v => videos.value = v)
.catch((err) => {
console.error(err);
console.error(err);
const title = 'Не удалось загрузить видео с Anime.365';
const message: string = err.code === 403
? 'Перейдите в настройки и обновите ключ доступа'
: err.message !== undefined
? err.message
: typeof err === 'string'
? err
: JSON.stringify(err);
const title = 'Не удалось загрузить видео с Anime.365';
const message: string = err.code === 403
? 'Перейдите в настройки и обновите ключ доступа'
: err.message !== undefined
? err.message
: typeof err === 'string'
? err
: JSON.stringify(err);
showErrorMessage({title, message});
showErrorMessage({title, message});
return [];
});
return [];
});
};
watchEffect(() => {
if (selectedTranslation.value && selectedTranslation.value.id) {
return loadVideoSources();
}
});
const onSourceError = () =>
clearVideosCache(selectedTranslation.value.id)
.then(() => loadVideoSources());
const onSourceError = () => {
if (!selectedTranslation.value) {
return;
}
clearVideosCache(selectedTranslation.value.id).then(() => loadVideoSources());
};
const isSidePanelOpened = ref(false);
Expand Down
2 changes: 1 addition & 1 deletion packages/renderer/src/utils/dialogs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createIpcClient} from '/@/ipc';


export function showErrorMessage({title = 'Ошибка', message = 'Неизвестная ошибка'}): Promise<void> {
export function showErrorMessage({title = 'Ошибка', message = 'Неизвестная ошибка'}: {title: string, message: string}): Promise<void> {
const dialog = createIpcClient('DialogsControllers');
return dialog.showError(title, message);
}
Expand Down
135 changes: 132 additions & 3 deletions packages/renderer/src/utils/translationRecomendations.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type {Translation} from '/@/utils/videoProvider';
import type {DBSchema, IDBPDatabase } from 'idb';
import type {Translation, TranslationAuthor} from '/@/utils/videoProvider';
import type {DBSchema, IDBPDatabase} from 'idb';
import {openDB} from 'idb';
import type {IndexKey} from 'idb/build/esm/entry';
import type {DeepReadonly} from 'vue';


interface TranslationRecommendations extends DBSchema {
preferences: {
value: Translation & {seriesId: number};
value: Translation & { seriesId: number };
key: number;
indexes: {
'by-type': 'sub' | 'voice'
Expand Down Expand Up @@ -41,3 +43,130 @@ export async function savePreferredTranslation(seriesId: NumberLike, translation

return (await getDB()).put('preferences', {...translation, seriesId});
}


function getPreferredTranslationType(): Promise<'sub' | 'voice'> {
return Promise.all([
getDB().then(db => db.countFromIndex('preferences', 'by-type', 'sub')),
getDB().then(db => db.countFromIndex('preferences', 'by-type', 'voice')),
]).then(([subCount, voiceCount]) => subCount > voiceCount ? 'sub' : 'voice');
}


//
// async function keyCount<IndexName extends IndexNames<TranslationRecommendations, 'preferences'>>(indexName: IndexName) {
// const db = await getDB();
// const transaction = db.transaction('preferences', 'readonly');
// const index = transaction.objectStore('preferences').index(indexName);
//
// const countMap = new Map<IndexKey<TranslationRecommendations, 'preferences', IndexName>, number>();
//
// let cursor = await index.openKeyCursor();
// while (cursor) {
// const key = cursor.key as IndexKey<TranslationRecommendations, 'preferences', IndexName>;
// const value = countMap.get(key) || 0;
// countMap.set(key, value + 1);
// cursor = await cursor.continue();
// }
//
// await transaction.done;
//
// return countMap;
// }


/**
* Возвращает массив авторов, отсортированный по приоритету для пользователя
*/
async function getPreferredTranslationAuthorsByType(preferredType: IndexKey<TranslationRecommendations, 'preferences', 'by-type'>): Promise<TranslationAuthor[]> {
const all = await getDB().then(db => db.getAllFromIndex('preferences', 'by-type', preferredType));
if (all.length === 0) {
return [];
}

if (all.length === 1) {
return [all[0].author];
}

const map = new Map<TranslationAuthor['id'], { author: TranslationAuthor, count: number }>();

all.forEach(({author}) => {
let saved = map.get(author.id);
if (!saved) {
saved = {
author,
count: 0,
};
}

saved.count += 1;
map.set(author.id, saved);
});

return Array.from(map.values())
.filter(s => s.count > 1)
.sort((a, b) => b.count - a.count)
.map(s => s.author);
}


type MaybeReadonly<T> = DeepReadonly<T> | T


export async function getPreferredTranslationFromList<T extends MaybeReadonly<Translation>>(seriesId: number, translations: T[]): Promise<T | undefined> {

if (!translations || translations.length === 0) {
return undefined;
}

/**
* Сохранённый перевод для выбранного аниме
*/
const reference = await getDB().then(db => db.get('preferences', seriesId));

// Попытка быстро вернуть перевод, если сразу найдётся совпадение по автору и типу
if (reference) {
const preferredTranslation = translations.find(t => t.type === reference.type && t.author.isEqual(reference.author));
if (preferredTranslation) {
return preferredTranslation;
}
}

/**
* Предпочтительный для пользователя {@link TranslationType тип} перевода
*/
const preferredType = reference?.type || await getPreferredTranslationType();

/**
* Массив переводов предпочитаемого {@link TranslationType типа}
*/
const typedTranslations = translations.filter(t => t.type === preferredType);

// Если нет ни одного перевода предпочитаемого типа -- просто вернуть первый (любого типа) из всего списка
if (typedTranslations.length === 0) {
return translations[0];
}

// Быстро вернуть перевод предпочитаемого типа если он всего один
if (typedTranslations.length === 1) {
return typedTranslations[0];
}

const preferredAuthors = await getPreferredTranslationAuthorsByType(preferredType);

// Если нет предпочитаемых авторов -- вернуть первый подходящий перевод
if (preferredAuthors.length === 0) {
return typedTranslations[0];
}

// Попытка найти среди доступных перевод от одного из предпочитаемых авторов по порядку их приоритета
for (const preferredAuthor of preferredAuthors) {
const preferredTranslation = translations.find(t => t.author.isEqual(preferredAuthor));
if (preferredTranslation) {
return preferredTranslation;
}
}

// Если ни один из вариантов поиска не дал результат -- вернуть первый доступный перевод
return typedTranslations[0];
}
55 changes: 44 additions & 11 deletions packages/renderer/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,30 @@ import {VitePWA} from 'vite-plugin-pwa';

const PACKAGE_ROOT = __dirname;


/**
* Vite looks for `.env.[mode]` files only in `PACKAGE_ROOT` directory.
* Therefore, you must manually load and set the environment variables from the root directory above
*/
loadAndSetEnv(process.env.MODE, process.cwd());

const waitOnlinePlugin = {
requestWillFetch: ({request}) => {
if (navigator.onLine) {
return Promise.resolve(request);
}

return new Promise(r => {
const c = setInterval(() => {
if (navigator.onLine) {
clearInterval(c);
r(request);
}
}, 2000);
});
},
};

/**
* @see https://vitejs.dev/config/
*/
Expand Down Expand Up @@ -63,16 +81,7 @@ export default defineConfig({
},
plugins: [
vue(),
copy({
hook: 'writeBundle',
targets: [{
src: [
'packages/renderer/src/components/WatchPage/VideoPlayer/libass-wasm/subtitles-octopus-worker.data',
'packages/renderer/src/components/WatchPage/VideoPlayer/libass-wasm/subtitles-octopus-worker.wasm',
],
dest: 'packages/renderer/dist',
}],
}),

VitePWA({
mode: 'development',
injectRegister: 'script',
Expand All @@ -84,15 +93,39 @@ export default defineConfig({
urlPattern: /^https:\/\/smotret-anime\.online\/api\/.*/,
handler: 'CacheFirst',
options: {
plugins: [waitOnlinePlugin],
cacheName: 'sm-api-calls',
expiration: {
maxAgeSeconds: 60 * 60,
},
},
},

{
urlPattern: /^https:\/\/sub\.smotret-anime\.online\/.*/,
handler: 'CacheFirst',
options: {
plugins: [waitOnlinePlugin],
cacheName: 'sm-subtitles',
expiration: {
maxEntries: 10,
},
},
},
],
},
}),

copy({
verbose: true,
hook: 'writeBundle',
targets: [{
src: [
'./src/components/WatchPage/VideoPlayer/libass-wasm/subtitles-octopus-worker.data',
'./src/components/WatchPage/VideoPlayer/libass-wasm/subtitles-octopus-worker.wasm',
],
dest: './dist',
}],
}),
],
});

0 comments on commit b13e609

Please sign in to comment.