diff --git a/README.md b/README.md index 9b5b826..573eb0d 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,4 @@ ___ + \ No newline at end of file diff --git a/javascript/auto/download_a.js b/javascript/auto/download_a.js new file mode 100644 index 0000000..3ab0460 --- /dev/null +++ b/javascript/auto/download_a.js @@ -0,0 +1,203 @@ +import { Sleep } from "../modules/funcitons.js"; +import { ShowInfo } from "../modules/Popup.js"; +import { UserRates } from "../modules/ShikiAPI.js"; +import { User } from "../modules/ShikiUSR.js"; + +const SYNC_ENABLE = $PARAMETERS.anime.syncdata; +const AUTO_ENABLE = $PARAMETERS.download.dautoset; + +(async () => { + const key = "download-a"; + + /** + * @type {[{id_ur: number, episodes: number, downloaded: [{episode: number, date:string}], duration: number}] | []} + */ + const data = JSON.parse(localStorage.getItem(key)) || []; + + if (data.length === 0 || !User.authorized || !AUTO_ENABLE) + return; + + let set = []; + + const a = new Date(); + + for (let i = 0; i < data.length; i++) { + const element = data[i]; + let allowed = true; + for (let e = 0; e < element.downloaded.length; e++) { + const episode = element.downloaded[e]; + let b = new Date(episode.date); + + b.setMinutes(b.getMinutes() + element.duration); + b.setTime(b.getTime() + (12 * 60 * 60 * 1000)); + + if (a.getTime() > b.getTime() && allowed) { + set.push({ id: element.id_ur, episode: episode.episode }); + } else { + allowed = false; + set = set.filter(item => item.id !== element.id_ur); + } + } + } + + set = Filter(set); + + let ids = []; + + for (let i = 0; i < set.length; i++) { + const element = set[i]; + + await Sleep(1000); + const completed = await Update(element.id, element.episode, element.count); + + if (completed) + ids.push(element.id); + } + + for (let i = 0; i < ids.length; i++) { + const element = ids[i]; + const index = data.findIndex(x => x.id_ur == element); + if (index != -1) { + data.splice(index, 1); + } + } + + if (ids.length != 0) + ShowInfo(`Обновлено ${ids.length} загруженных аниме`, key); + + if (data.length === 0) + return localStorage.removeItem(key); + localStorage.setItem(key, JSON.stringify(data)); +})(); + +function Update(id, episode, count) { + const status = ["watching", "rewatching", "planned", "dropped"] + return new Promise((resolve) => { + UserRates.show(id, async (response) => { + if (response.failed && response.status == 429) { + await Sleep(1000); + return resolve(Update(id)); + } + + if (response.failed) { + return resolve(false); + } + + if (response.episodes < episode && status.includes(response.status) && (response.episodes + count + 1) === episode) { + return resolve((await SetWatched(episode, response))); + } else { + return resolve(true); + } + }).GET(); + }) +} + + +/** + * + * @param {*} data + * @returns {[{ id: number, episode: number, count: number }]} + */ +function Filter(data) { + // Создаем объект для хранения эпизодов по каждому id + const ids = {}; + + // Разбиваем данные по id + for (const item of data) { + if (!ids[item.id]) { + ids[item.id] = { episodes: [] }; + } + ids[item.id].episodes.push(item.episode); + } + + const retData = []; + + // Проходимся по каждому id и находим максимальный эпизод + for (const id in ids) { + if (Object.hasOwnProperty.call(ids, id)) { + const episodes = ids[id].episodes; + episodes.sort((a, b) => a - b); + + // Находим максимальный эпизод и считаем количество эпизодов после него + let maxEpisode = episodes[0]; + let count = 0; + for (let i = 0; i < episodes.length; i++) { + if (episodes[i] >= maxEpisode && episodes[i] - maxEpisode === 1) { + maxEpisode = episodes[i]; + count++; + } + } + + // Добавляем данные в результирующий массив + retData.push({ id: parseInt(id), episode: maxEpisode, count }); + } + } + + return retData; +} + +/** + * Сохраняет данные аниме и обновляет комментарий + * @param {number} e - Эпизод аниме + * @param {Object} user_rate - данные прользователя об аниме + */ +function SetWatched(e, user_rate) { + return new Promise((resolve) => { + let body = { "user_rate": { "episodes": e } }; + + if (user_rate.status == "planned" || user_rate.status == "dropped") + body.user_rate["status"] = "watching"; + + if (localStorage.getItem(user_rate.target_id)) { + /**@type {{kodik_episode:number, kodik_dub:number, date_update:number} || null} */ + const data = JSON.parse(localStorage.getItem(user_rate.target_id)) || null; + + if (data != null) { + data.kodik_episode = e; + data.date_update = new Date(); + + localStorage.setItem(user_rate.target_id, JSON.stringify(data)); + + if (SYNC_ENABLE) { + const regex = /\[tunime-sync:(\d+):(\d+):"(.+?)"]/; + let match = ""; + + if (user_rate.text) { + match = user_rate.text.match(regex); + } + + if (match) { + user_rate.text = user_rate.text.replace(match[0], ''); + } + + if (user_rate.text) { + user_rate.text = user_rate.text.trim(); + } else { + user_rate.text = ""; + } + + user_rate.text += `\r\n[tunime-sync:${data.kodik_episode}:${data.kodik_dub}:${JSON.stringify(data.date_update)}]`; + body.user_rate["text"] = user_rate.text; + } + } + } + return resolve(Fetch(user_rate.id, body)); + }); +} + +function Fetch(id, body) { + return new Promise((resolve) => { + UserRates.show(id, async (response) => { + if (response.failed && response.status == 429) { + await Sleep(1000); + return resolve(Fetch(id, body)); + } + + if (response.failed) { + return resolve(false); + } + + return resolve(true); + }).PATCH(body); + }); +} \ No newline at end of file diff --git a/javascript/engine/event_handler.js b/javascript/engine/event_handler.js index 8dffa7e..366c813 100644 --- a/javascript/engine/event_handler.js +++ b/javascript/engine/event_handler.js @@ -38,11 +38,34 @@ let app_installed_device = { if (key == 'version') continue; - if(events_handler_list[key].completed == false && events_handler_list[key].target == app_current_page){ + if (events_handler_list[key].completed == false && events_handler_list[key].target == app_current_page) { CallEventHandler(key); } } } + + (() => { + const key = "download-a"; + const File = "download_a.js"; + /** + * @type {[{id_ur: number, episodes: number, downloaded: [{episode: number, date:string}], duration: number}] | []} + */ + const data = JSON.parse(localStorage.getItem(key)) || []; + + if (data.length === 0) + return; + + OpenScript(File); + })(); + + function OpenScript(source) { + window.onload = function () { + const script = document.createElement('script'); + script.src = `/javascript/auto/${source}`; + script.type = "module"; + document.body.appendChild(script); + }; + } })(); /** @@ -110,7 +133,7 @@ function LoadEventsHandlerList() { */ function CallEventHandler(params) { const events = { - onStart: function (){ + onStart: function () { window.location.href = "login.html"; } } diff --git a/javascript/modules/ShikiAPI.js b/javascript/modules/ShikiAPI.js index 75e8167..c66d828 100644 --- a/javascript/modules/ShikiAPI.js +++ b/javascript/modules/ShikiAPI.js @@ -209,6 +209,14 @@ export const UserRates = { const response = await request.fetch(); event(response); return response; + }, + + GET: async () => { + request.setMethod("GET"); + request.setHeaders(Headers.bearer()); + const response = await request.fetch(); + event(response); + return response; } } }, diff --git a/javascript/pages/settings.js b/javascript/pages/settings.js index 3173c62..bd2592a 100644 --- a/javascript/pages/settings.js +++ b/javascript/pages/settings.js @@ -218,7 +218,13 @@ const Parameters = [ type: 'boolean', param: 'dautosave', name: 'Автосохранение', - description: 'После загрузки автоматически сохраняет файл.' + description: 'После загрузки автоматически сохраняет файл.' + }, + { + type: 'boolean', + param: 'dautoset', + name: 'Автоотметки', + description: 'Отмечать загруженые аниме через 12 часов + продолжительность аниме.' } ] } diff --git a/javascript/pages/watch/mod_download.js b/javascript/pages/watch/mod_download.js index c8f333f..88047b6 100644 --- a/javascript/pages/watch/mod_download.js +++ b/javascript/pages/watch/mod_download.js @@ -1,365 +1,571 @@ +import { ScrollElementWithMouse, Sleep } from "../../modules/funcitons.js"; import { Tunime } from "../../modules/TunimeApi.js"; import { WindowManagement } from "../../modules/Windows.js"; -import { ScrollElementWithMouse } from "../../modules/funcitons.js"; import { Player } from "./mod_player.js"; +import { Anime } from "./mod_resource.js"; +import { UserRate } from "./mod_urate.js"; + +let AutoSave = $PARAMETERS.download.dautosave; + +class Automation { + constructor(downl) { + this.downl = downl; + this.key = "download-a"; + this.Data = JSON.parse(localStorage.getItem(this.key)) || []; + this.Date = new Date().toJSON(); + this.Autoset = $PARAMETERS.download.dautoset; + } -let loaded = false; // Загружены ли эпизоды -let selected = 1; // Выбранный эпизод -let elementSelected = undefined; //Последний выбранный елемент + Show() { + /**@type {[{id:number, episode: [number]}]} */ + let localData = JSON.parse(sessionStorage.getItem(this.key)) || []; + const ur = UserRate().Get(); + const index = localData.findIndex(x => x.id == ur.id); -let downloadLink = undefined; // Локальная ссылка для загрузки файла + if (index === -1) { + return; + } -let startTime, endTime; + for (let i = 0; i < localData[index].episode.length; i++) { + const ep = localData[index].episode[i]; + if ($(`.d-episode[data-e="${ep}"] > .downloaded`).length === 0) { + $(`.d-episode[data-e="${ep}"]`).append(``); + } + } + } -const _data = { - link: undefined, - name: "Anime", - translation: undefined, -} + Set(ep) { + const duration = Anime.duration; + const ur = UserRate().Get(); -let totalFiles = 0; -let downloadedFiles = 0; + if (ur !== null && ur.episodes < ep && this.Autoset) { + let downlData = { id_ur: undefined, episodes: undefined, downloaded: [], duration: duration }; + let index = this.Data.findIndex(x => x.id_ur === ur.id); -/** - * Создает эпизоды в окне загрузке - * @returns ? ничего не возвращяет - */ -function LoadingEpisodes() { - if (loaded) { - return; - } - loaded = true; - const e = Player().episodes.last_episode; - for (let i = 0; i < e; i++) { - const count = i + 1; - $('.window-body-fs > .download-episode > .down-value').append(`${count}EP`); - } + if (index !== -1) { + downlData = this.Data[index]; + this.Data.splice(index, 1); + } - if (e === undefined) { - $('.download-episode').hide(); - } + downlData.id_ur = ur.id; + downlData.episodes = ur.episodes; - $('.window-body-fs > .download-episode > .down-value > .down-episode').on('click', function (e) { - const element = $(e.currentTarget); - const index = element.attr('data-index'); - SelectEpisode(index, true); - }); -} + index = downlData.downloaded.findIndex(x => x.episode === ep); -/** - * Выбирает эпизод - * @param {number} val - выбранный эпизод - * @param {boolean} user - пользователь вызвал - * @returns ? ничего - */ -function SelectEpisode(val, user = false) { - if (!val && selected == val) { - return; - } - selected = val; - const element = $(".download-episode > .down-value > .down-episode")[val - 1]; - if (elementSelected != undefined) { - anime({ - targets: elementSelected, - color: "#555657", - easing: "easeOutElastic(1, 1)", - }) - } - elementSelected = element; - const left = $(element).position().left; - - anime({ - targets: ".sel-down", - left: left, - easing: "easeOutElastic(1, 1)", - complete: function () { - if (!user) { - AutoScrollToEpisode(); + if (index === -1) { + downlData.downloaded.push({ episode: ep, date: this.Date }); } - }, - }); - anime({ - targets: element, - color: "#020202", - easing: "easeOutElastic(1, 1)", - }); -} + this.Data.push(downlData); + localStorage.setItem(this.key, JSON.stringify(this.Data)); + } -/** - * Скроллит к выбранному эпизоду - * @returns ? ничего - */ -function AutoScrollToEpisode() { - let SelPos = $('.down-value > .sel-down').position(); - const WidthEpisodes = $('.download-episode').width(); - const sizeEpisode = (55 + 3); - if ((WidthEpisodes / 2) > SelPos.left) { - return; - } - anime({ - targets: '.download-episode', - scrollLeft: (SelPos.left - (WidthEpisodes / 2) + sizeEpisode), - duration: 500, - easing: 'easeInOutQuad' - }); -} + /**@type {[{id:number, episode: [number]}]} */ + let localData = JSON.parse(sessionStorage.getItem(this.key)) || []; + const index = localData.findIndex(x => x.id == ur.id); + let data = { id: ur.id, episode: [] }; -function SetButtonStatus(status) { - const btn = $(`.window-futter > #btn-download`); - if (status == "loading") { - btn.attr('data-status', status) - btn.text("Загрузка..."); - btn.addClass("disabled"); - } - if (status == "ready") { - btn.attr('data-status', status); - btn.text("Загрузить"); - btn.removeClass("disabled"); - } - if (status == "candown") { - btn.attr('data-status', status); - btn.text("Сохранить"); - btn.removeClass("disabled"); - } -} + if (index !== -1) { + data = localData[index]; + localData.splice(index, 1) + } -function _downloadAnime(e) { - if ($(e).attr('data-status') == "loading") { - return; - } - if ($(e).attr('data-status') == "candown") { - DownloadLocalVideo(); - return; - } - if ($(e).attr('data-status') == "ready") { - SetButtonStatus("loading"); - downloadedFiles = 0; - totalFiles = 0; - $('.progress-download > .value').css({ width: `${0}%` }); // Обновляем индикатор загрузки - GetM3U8Links(); - return; + data.episode.push(ep); + localData.push(data); + + if ($(`.d-episode[data-e="${ep}"] > .downloaded`).length === 0) { + $(`.d-episode[data-e="${ep}"]`).append(``); + } + + sessionStorage.setItem(this.key, JSON.stringify(localData)); } } -async function GetM3U8Links() { - let link = `${_data.link}?episode=${selected}`; - if (!link.includes("http")) { - link = `https:${link}`; +class DownloadAnime { + #abortet = false; + constructor(index, data, downl) { + this.index = index; + this.data = data; + this.downl = downl; + + this.eProgress = $('.progress-download > .value'); + this.eCount = $('.progress-download > .value > .percent'); + + const count = `${0}%` + + this.eProgress.css({ width: count }); + this.eCount.text(count); + + this.Stats = { + total: 0, + downloaded: 0 + }; + + this.startTime = 0; + this.endTime = 0; + this.typeDownload = $PARAMETERS.download.dasync; + this.downloadLink = undefined; } - const data = await Tunime.Source(link); - if (data) { - _localDownload(data); + + Abort() { + this.#abortet = true; } -} -function GetQualityDownload(data) { - let allowQuality = ['720', '480', '360']; - let currentQuality = $PARAMETERS.download.dquality; + async Download() { + this.#OnLoading.forEach(event => event()); + + let link = `${this.data.link}?episode=${this.index}`; - //Записываем только досутпные разрешения - for (let i = 0; i < allowQuality.length; i++) { - const e = allowQuality[i]; - if (data[e].length == 0) { - allowQuality.splice(i, 1); + if (!link.includes("http")) { + link = `https:${link}`; + } + + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; } - } - let idQuality = allowQuality.findIndex(x => x == currentQuality); + const data = await Tunime.Source(link); - if (idQuality == -1) { - if (allowQuality.length != 0) { - currentQuality = allowQuality[0]; + if (data) { + this.#LocalDownload(data); } else { - return -1; + this.#OnError.forEach((event) => { event('critical', 'Ошибка получение данных Tunime.') }); } } - return currentQuality; -} + DownloadBlob() { + const translation = `-${this.data.translation}`; + // Создаем ссылку для скачивания + const dL = document.createElement('a'); + dL.href = this.downloadLink; + dL.download = `${this.data.name}-${this.index}${translation}.ts`; + // Автоматически нажимаем на ссылку для скачивания + dL.click(); + // Очищаем ссылку и удаляем ее из DOM\ + URL.revokeObjectURL(dL.href); + this.#OnCompleted.forEach(event => event(this.index)); + } -function _localDownload(data) { - const quality = GetQualityDownload(data); + #LocalDownload(data) { + const quality = GetQualityDownload(data, this.downl.Quality); - if (quality == -1) { - //Недоступно не одного видео для скачивания - return; - } + if (quality == -1) { + return this.#OnError.forEach((event) => { event('critical', 'Ошибка выбора качества видео.') }); + } - const url = data[quality][0].src.indexOf("http") != -1 ? data[quality][0].src : "https:" + data[quality][0].src; - // Строка, которую нужно удалить - const searchString = `${quality}.mp4:hls:manifest.m3u8`; + const url = data[quality][0].src.indexOf("http") != -1 ? data[quality][0].src : "https:" + data[quality][0].src; + // Строка, которую нужно удалить + const searchString = `${quality}.mp4:hls:manifest.m3u8`; + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } - fetch(url) - .then(response => { - // Удалите подстроку из URL + fetch(url).then(response => { + // Удалить подстроку из URL const urlkodik = response.url.substring(0, response.url.indexOf(searchString)); + response.text().then(async (m3u8Content) => { // data содержит текст манифеста M3U8 const tsUrls = m3u8Content.split('\n').filter(line => line.trim().endsWith('.ts')); + // Сохраняем время начала загрузки - startTime = new Date().getTime(); + this.startTime = new Date().getTime(); - if ($PARAMETERS.download.dasync) { - AsyncDownloadVideo(tsUrls, urlkodik); + if (this.typeDownload) { + this.#AsyncDownload(tsUrls, urlkodik) } else { - DownloadVideo(tsUrls, urlkodik); + this.#SyncDonwload(tsUrls, urlkodik); } + }).catch(error => { - console.error('Ошибка при загрузке M3U8: ', error); + return this.#OnError.forEach((event) => { event('critical', 'Ошибка загрузки m3u8 файла.') }); }); }); -} -async function AsyncDownloadVideo(tsUrls, urlkodik) { - const downloadPromises = []; - totalFiles = tsUrls.length; + function GetQualityDownload(data, currentQuality) { + let allowQuality = ['720', '480', '360']; + + //Записываем только досутпные разрешения + for (let i = 0; i < allowQuality.length; i++) { + const e = allowQuality[i]; + if (data[e].length == 0) { + allowQuality.splice(i, 1); + } + } + + let idQuality = allowQuality.findIndex(x => x == currentQuality); + + if (idQuality == -1) { + if (allowQuality.length != 0) { + currentQuality = allowQuality[0]; + } else { + return -1; + } + } + + return currentQuality; + } + } + + async #AsyncDownload(tsUrls, urlkodik) { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + const downloadPromises = []; + this.Stats.total = tsUrls.length; + + for (let i = 0; i < tsUrls.length; i++) { + try { + const tsUrl = tsUrls[i]; + downloadPromises.push(this.#DownloadTsFile(urlkodik + tsUrl)) + } catch (error) { + this.#OnError.forEach((event) => { event('warning', `Ошибка загрузки фрагмента ${i}.`) }); + console.error(`Failed to fetch ${tsUrls[i]}: ${error.message}`); + } + } - for (let i = 0; i < tsUrls.length; i++) { try { - const tsUrl = tsUrls[i]; - downloadPromises.push(downloadTsFile(urlkodik + tsUrl)); + const tsBlobs = await Promise.all(downloadPromises); + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + // Завершаем загрузку + this.endTime = new Date().getTime(); + // Вычисляем время загрузки + const uploadTime = (this.endTime - this.startTime) / 1000; // в секундах + console.log(uploadTime); + + if (tsBlobs.length === 0) { + return this.#OnError.forEach((event) => { event('critical', `Не удалось загрузить ни один фрагмент.`) }); + } + + const mergedBlob = new Blob(tsBlobs, { type: 'video/mp2t' }); + this.downloadLink = URL.createObjectURL(mergedBlob); + + this.#OnCanDownload.forEach((event) => event()); + } catch (error) { - console.error(`Failed to fetch ${tsUrl[i]}: ${error.message}`); + console.error('Ошибка при загрузке M3U8: ', error); + return this.#OnError.forEach((event) => { event('critical', `Ошибка при загрузке M3U8.`) }); } } - try { - const tsBlobs = await Promise.all(downloadPromises); + async UpdateProgress() { + const progress = (this.Stats.downloaded / this.Stats.total) * 100; + this.eProgress.css({ width: `${progress}%` }); // Обновляем индикатор загрузки + this.eCount.text(`${progress.toFixed(0)}%`); + } - // Завершаем загрузку (это симуляция, на самом деле должно быть событие окончания загрузки файла) - endTime = new Date().getTime(); + async #DownloadTsFile(tsUrl) { + const tsResponse = await this.#fetchWithRetry(tsUrl); + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + const tsBlob = await tsResponse.blob(); + this.Stats.downloaded++; + this.UpdateProgress(); + return tsBlob; + } + + async #fetchWithRetry(url, retryCount = 25) { + try { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + return response; + } catch (error) { + console.error(`Error fetching ${url}: ${error.message}`); + if (retryCount > 0) { + console.log(`Retrying in 1 second... (${retryCount} attempts left)`); + await Sleep(1000); + return this.#fetchWithRetry(url, retryCount - 1); + } else { + throw new Error(`Failed to fetch ${url} after multiple attempts`); + } + } + } + async #SyncDonwload() { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + let tsBlobs = []; + this.Stats.total = tsUrls.length; + for (let i = 0; i < tsUrls.length; i++) { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + try { + const tsUrl = tsUrls[i]; + const tsResponse = await this.#fetchWithRetry(urlkodik + tsUrl); + const tsBlob = await tsResponse.blob(); + tsBlobs.push(tsBlob); + this.Stats.downloaded++; + this.UpdateProgress(); + } catch (error) { + this.#OnError.forEach((event) => { event('warning', `Ошибка загрузки фрагмента ${i}.`) }); + } + } + + // Завершаем загрузку + this.endTime = new Date().getTime(); // Вычисляем время загрузки - const uploadTime = (endTime - startTime) / 1000; // в секундах + const uploadTime = (this.endTime - this.startTime) / 1000; // в секундах console.log(uploadTime); if (tsBlobs.length === 0) { - console.log('Не удалось загрузить ни один файл .ts!'); - return; + return this.#OnError.forEach((event) => { event('critical', `Не удалось загрузить ни один фрагмент.`) }); } const mergedBlob = new Blob(tsBlobs, { type: 'video/mp2t' }); - downloadLink = URL.createObjectURL(mergedBlob); + this.downloadLink = URL.createObjectURL(mergedBlob); - DownloadCompleated(); - } catch (error) { - console.error('Ошибка при загрузке M3U8: ', error); + this.#OnCanDownload.forEach((event) => event()); } -} -async function DownloadVideo(tsUrls, urlkodik) { - let tsBlobs = []; - for (let i = 0; i < tsUrls.length; i++) { - try { - const tsUrl = tsUrls[i]; - const tsResponse = await fetchWithRetry(urlkodik + tsUrl); - const tsBlob = await tsResponse.blob(); - tsBlobs.push(tsBlob); - const progress = ((i + 1) / tsUrls.length) * 100; - $('.progress-download > .value').css({ width: `${progress}%` }); - } catch (error) { - console.error(`Failed to fetch ${tsUrls[i]}: ${error.message}`); + Deprecate() { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + + #OnLoading = []; + #OnCanDownload = []; + #OnCompleted = []; + #OnError = []; + #OnAbbortet = []; + + /** + * + * @param {'loading' | 'candownload' | 'completed' | 'error' | 'abbortet'} name + * @param {function} event + */ + On(name, event = () => { }) { + if (name === 'loading') { + this.#OnLoading.push(event); + } else if (name === 'candownload') { + this.#OnCanDownload.push(event); + } else if (name === 'completed') { + this.#OnCompleted.push(event); + } else if (name === 'error') { + this.#OnError.push(event); + } else if (name === 'abbortet') { + this.#OnAbbortet.push(event); } } +} - // Завершаем загрузку (это симуляция, на самом деле должно быть событие окончания загрузки файла) - endTime = new Date().getTime(); +class Loading { + #loaded = false; - // Вычисляем время загрузки - const uploadTime = (endTime - startTime) / 1000; // в секундах - console.log(uploadTime); + constructor(downl) { + /** + * @type {Download} + */ + this.Download = downl; + } - if (tsBlobs.length === 0) { - console.log('Не удалось загрузить ни один файл .ts!'); - return; + get IsLoaded() { + return this.#loaded; } - const mergedBlob = new Blob(tsBlobs, { type: 'video/mp2t' }); - downloadLink = URL.createObjectURL(mergedBlob); + Load() { + if (this.#loaded) + return; + this.#loaded = true; - DownloadCompleated(); -} + const e = Player().episodes.last_episode; + if (e !== undefined && e > 1) + $('.wrapper-episodes-d').removeClass('hide'); -function DownloadCompleated() { - SetButtonStatus("candown"); - if ($PARAMETERS.download.dautosave) { - DownloadLocalVideo(); - } -} + $('.wrapper-episodes-d > .episodes-download').empty(); -function DownloadLocalVideo() { - const translation = `-${_data.translation}`; - // Создаем ссылку для скачивания - const dL = document.createElement('a'); - dL.href = downloadLink; - dL.download = `${_data.name}-${selected}${translation}.ts`; - // Автоматически нажимаем на ссылку для скачивания - dL.click(); - // Очищаем ссылку и удаляем ее из DOM - URL.revokeObjectURL(dL.href); - SetButtonStatus("ready"); -} + for (let i = 0; i < e; i++) { + const c = i + 1; + $('.wrapper-episodes-d > .episodes-download').append(`